Ai đó có thể giải thích Bộ chuyển đổi Clojure cho tôi bằng thuật ngữ Đơn giản không?


100

Tôi đã cố gắng đọc về điều này nhưng tôi vẫn không hiểu giá trị của chúng hoặc những gì chúng thay thế. Và họ có làm cho mã của tôi ngắn hơn, dễ hiểu hơn hay không?

Cập nhật

Rất nhiều người đã đăng các câu trả lời, nhưng sẽ thật tuyệt khi xem các ví dụ về việc có và không có bộ chuyển đổi cho một thứ rất đơn giản, mà ngay cả một kẻ ngốc như tôi cũng có thể hiểu được. Tất nhiên trừ khi các bộ chuyển đổi cần một mức độ hiểu biết cao nhất định, trong trường hợp đó tôi sẽ không bao giờ hiểu chúng :(

Câu trả lời:


75

Đầu dò là công thức phải làm với một chuỗi dữ liệu mà không cần biết trình tự bên dưới là gì (cách thực hiện). Nó có thể là bất kỳ kênh seq, kênh không đồng bộ hoặc có thể có thể quan sát được.

Chúng có thể kết hợp và đa hình.

Lợi ích là, bạn không phải triển khai tất cả các tổ hợp tiêu chuẩn mỗi khi nguồn dữ liệu mới được thêm vào. Lặp đi lặp lại. Do đó, bạn với tư cách là người dùng có thể sử dụng lại các công thức đó trên các nguồn dữ liệu khác nhau.

Cập nhật quảng cáo

Trước phiên bản 1.7 của Clojure, bạn có ba cách để viết truy vấn luồng dữ liệu:

  1. cuộc gọi lồng nhau
    (reduce + (filter odd? (map #(+ 2 %) (range 0 10))))
  1. thành phần chức năng
    (def xform
      (comp
        (partial filter odd?)
        (partial map #(+ 2 %))))
    (reduce + (xform (range 0 10)))
  1. macro luồng
    (defn xform [xs]
      (->> xs
           (map #(+ 2 %))
           (filter odd?)))
    (reduce + (xform (range 0 10)))

Với đầu dò, bạn sẽ viết nó như sau:

(def xform
  (comp
    (map #(+ 2 %))
    (filter odd?)))
(transduce xform + (range 0 10))

Tất cả chúng đều làm như nhau. Sự khác biệt là bạn không bao giờ gọi trực tiếp Transducers, bạn chuyển chúng cho một chức năng khác. Đầu dò biết phải làm gì, chức năng nhận đầu dò biết làm thế nào. Thứ tự của các tổ hợp giống như bạn viết nó bằng macro phân luồng (thứ tự tự nhiên). Bây giờ bạn có thể sử dụng lại xformvới kênh:

(chan 1 xform)

3
Tôi đang tìm kiếm một câu trả lời đi kèm với một ví dụ cho tôi thấy cách đầu dò giúp tôi tiết kiệm thời gian.
appshare.co

Họ không có nếu bạn không phải là Clojure hoặc một số người bảo trì luồng dữ liệu.
Aleš Roubíček

5
Nó không phải là một quyết định kỹ thuật. Chúng tôi chỉ sử dụng các quyết định dựa trên giá trị kinh doanh. "Chỉ cần sử dụng chúng" sẽ làm cho tôi bị sa thải
appshare.co

1
Bạn có thể có thời gian dễ dàng hơn để giữ công việc của mình nếu bạn trì hoãn việc cố gắng sử dụng đầu dò cho đến khi Clojure 1.7 được phát hành.
user100464

7
Bộ chuyển đổi dường như là một cách hữu ích để trừu tượng hóa các dạng khác nhau của các đối tượng có thể lặp lại. Những thứ này có thể không tiêu thụ được, chẳng hạn như seqs Clojure hoặc có thể tiêu hao (chẳng hạn như kênh không đồng bộ). Về mặt này, đối với tôi, có vẻ như bạn sẽ được hưởng lợi rất nhiều từ việc sử dụng bộ chuyển đổi, ví dụ: bạn chuyển từ triển khai dựa trên seq sang triển khai core.async bằng cách sử dụng các kênh. Bộ chuyển đổi nên cho phép bạn giữ nguyên cốt lõi logic của bạn không thay đổi. Sử dụng xử lý dựa trên trình tự truyền thống, bạn sẽ phải chuyển đổi điều này để sử dụng đầu dò hoặc một số bộ tương tự lõi không đồng bộ. Đó là trường hợp kinh doanh.
Nathan Davis

47

Bộ chuyển đổi cải thiện hiệu quả và cho phép bạn viết mã hiệu quả theo cách mô-đun hơn.

Đây là một hoạt động tốt .

So với việc soạn lời gọi cũ map, v.v. filter, reducebạn sẽ có hiệu suất tốt hơn vì bạn không cần phải tạo các bộ sưu tập trung gian giữa mỗi bước và lặp đi lặp lại các bộ sưu tập đó.

So với reducershoặc soạn thủ công tất cả các thao tác của bạn vào một biểu thức duy nhất, bạn sẽ dễ dàng sử dụng các tính năng trừu tượng hơn, tính mô đun tốt hơn và sử dụng lại các hàm xử lý.


2
Chỉ tò mò, bạn đã nói ở trên: "để xây dựng các bộ sưu tập trung gian giữa mỗi bước". Nhưng không phải "tập hợp trung gian" nghe giống như một mô hình phản đối? .NET cung cấp danh sách lười biếng, Java cung cấp các luồng lười biếng hoặc các tệp lặp do Guava điều khiển, Haskell lười biếng cũng phải có thứ gì đó lười biếng. Không ai trong số này yêu cầu map/ reducesử dụng các bộ sưu tập trung gian vì tất cả chúng đều xây dựng một chuỗi trình lặp. Tôi sai ở đâu ở đây?
Lyubomyr Shaydariv

3
Clojure mapfiltertạo các bộ sưu tập trung gian khi lồng vào nhau.
thợ ồn ào

4
Và ít nhất là liên quan đến phiên bản lười biếng của Clojure, vấn đề lười biếng là trực quan ở đây. Có, bản đồ và bộ lọc là lười biếng, cũng tạo ra các vùng chứa cho các giá trị lười biếng khi bạn chuỗi chúng. Nếu bạn không nắm chắc phần đầu, bạn sẽ không tạo ra các chuỗi lười lớn không cần thiết, nhưng bạn vẫn xây dựng những phần trừu tượng trung gian đó cho mỗi phần tử lười.
thợ ồn ào

Một ví dụ sẽ tốt đẹp.
appshare.co

8
@LyubomyrShaydariv Theo "bộ sưu tập trung gian", người sửa lỗi không có nghĩa là "lặp lại / sửa đổi toàn bộ bộ sưu tập, sau đó lặp lại / sửa đổi toàn bộ bộ sưu tập khác". Nghĩa là khi bạn lồng các lệnh gọi hàm trả về tuần tự, mỗi lệnh gọi hàm dẫn đến việc tạo ra một tuần tự mới. Việc lặp lại thực tế vẫn chỉ xảy ra một lần, nhưng có thêm mức tiêu thụ bộ nhớ và cấp phát đối tượng do các tuần tự lồng nhau.
erikprice

22

Bộ biến đổi là một phương tiện kết hợp để giảm chức năng.

Ví dụ: Các hàm giảm là các hàm nhận hai đối số: Một kết quả cho đến nay và một đầu vào. Chúng trả về một kết quả mới (cho đến nay). Ví dụ +: Với hai đối số, bạn có thể coi đối số đầu tiên là kết quả cho đến nay và đối số thứ hai là đầu vào.

Một bộ chuyển đổi giờ đây có thể sử dụng hàm + và biến nó thành hàm cộng hai lần (nhân đôi mọi đầu vào trước khi thêm vào). Đây là cách mà đầu dò sẽ trông như thế nào (theo hầu hết các thuật ngữ cơ bản):

(defn double
  [rfn]
  (fn [r i] 
    (rfn r (* 2 i))))

Để thay thế minh họa rfnbằng +để xem cách +được chuyển đổi thành cộng hai lần:

(def twice-plus ;; result of (double +)
  (fn [r i] 
    (+ r (* 2 i))))

(twice-plus 1 2)  ;-> 5
(= (twice-plus 1 2) ((double +) 1 2)) ;-> true

Vì thế

(reduce (double +) 0 [1 2 3]) 

bây giờ sẽ mang lại 12.

Các chức năng giảm do đầu dò trả về không phụ thuộc vào cách kết quả được tích lũy bởi vì chúng tích lũy với chức năng giảm được truyền cho chúng, không biết như thế nào. Ở đây chúng tôi sử dụng conjthay vì +. Conjnhận một tập hợp và một giá trị và trả về một tập hợp mới với giá trị đó được thêm vào.

(reduce (double conj) [] [1 2 3]) 

sẽ mang lại [2 4 6]

Chúng cũng không phụ thuộc vào loại nguồn đầu vào.

Nhiều đầu dò có thể được xâu chuỗi như một công thức (có thể thay thế) để biến đổi các chức năng khử.

Cập nhật: Vì bây giờ có một trang chính thức về nó, tôi thực sự khuyên bạn nên đọc nó: http://clojure.org/transducers


Lời giải thích tốt đẹp nhưng tôi đã sớm hiểu ra quá nhiều biệt ngữ, "Các chức năng giảm được tạo ra bởi đầu dò độc lập với cách kết quả được tích lũy".
appshare.co

1
Bạn nói đúng, từ được tạo ra không phù hợp ở đây.
Leon Grapenthin

Được rồi. Dù sao tôi hiểu rằng Transformers chỉ là một tối ưu hóa bây giờ, như vậy có lẽ không nên được sử dụng anyway
appshare.co

1
Chúng là một phương tiện kết hợp để giảm chức năng. Nơi nào khác mà bạn có? Đây không chỉ là một tối ưu hóa.
Leon Grapenthin

Tôi thấy câu trả lời này rất thú vị, nhưng tôi không rõ nó kết nối với đầu dò như thế nào (một phần vì tôi vẫn thấy chủ đề khó hiểu). Mối quan hệ giữa doublevà là transducegì?
Mars

21

Giả sử bạn muốn sử dụng một loạt các hàm để chuyển đổi một luồng dữ liệu. Unix shell cho phép bạn làm điều này với toán tử đường ống, ví dụ:

cat /etc/passwd | tr '[:lower:]' '[:upper:]' | cut -d: -f1| grep R| wc -l

(Lệnh trên đếm số người dùng có ký tự r viết hoa hoặc viết thường trong tên người dùng của họ). Điều này được thực hiện như một tập hợp các quy trình, mỗi quy trình đọc từ đầu ra của các quy trình trước đó, vì vậy có bốn luồng trung gian. Bạn có thể tưởng tượng một cách triển khai khác bao gồm năm lệnh thành một lệnh tổng hợp duy nhất, lệnh này sẽ đọc từ đầu vào và ghi đầu ra của nó chính xác một lần. Nếu các luồng trung gian đắt và thành phần rẻ, thì đó có thể là một sự đánh đổi tốt.

Điều tương tự cũng xảy ra với Clojure. Có nhiều cách để thể hiện một đường dẫn các phép biến đổi, nhưng tùy thuộc vào cách bạn thực hiện, bạn có thể kết thúc với các luồng trung gian truyền từ hàm này sang hàm tiếp theo. Nếu bạn có nhiều dữ liệu, việc soạn các hàm đó thành một hàm sẽ nhanh hơn. Bộ chuyển đổi giúp bạn dễ dàng thực hiện điều đó. Một cải tiến trước đó của Clojure, bộ giảm tốc, cho phép bạn làm điều đó, nhưng có một số hạn chế. Bộ chuyển đổi loại bỏ một số hạn chế đó.

Vì vậy, để trả lời câu hỏi của bạn, bộ chuyển đổi không nhất thiết phải làm cho mã của bạn ngắn hơn hoặc dễ hiểu hơn, nhưng mã của bạn có thể sẽ không dài hơn hoặc ít dễ hiểu hơn và nếu bạn đang làm việc với nhiều dữ liệu, bộ chuyển đổi có thể làm cho mã của bạn nhanh hơn.

Đây là một cái nhìn tổng quan về đầu dò khá tốt.


1
Ah, vì vậy các bộ chuyển đổi chủ yếu là tối ưu hóa hiệu suất, đó là những gì bạn đang nói?
appshare.co

@Zubair Vâng, đúng vậy. Lưu ý rằng việc tối ưu hóa vượt ra ngoài việc loại bỏ các luồng trung gian; bạn cũng có thể thực hiện các hoạt động song song.
user100464

2
Điều đáng nói pmaplà dường như không được chú ý. Nếu bạn đang mapping một hàm đắt tiền trên một chuỗi, việc thực hiện thao tác song song cũng dễ dàng như thêm "p". Không cần thay đổi bất kỳ điều gì khác trong mã của bạn và nó hiện có sẵn - không phải alpha, không phải beta. (Nếu chức năng tạo ra các chuỗi trung gian, thì các bộ chuyển đổi có thể nhanh hơn, tôi đoán vậy.)
Mars

10

Rich Hickey đã thuyết trình về 'Transducers' tại hội nghị Strange Loop 2014 (45 phút).

Anh ấy giải thích một cách đơn giản đầu dò là gì, với các ví dụ thực tế - xử lý túi trong sân bay. Ông tách biệt rõ ràng các khía cạnh khác nhau và đối lập chúng với các cách tiếp cận hiện tại. Cuối cùng, ông đưa ra lý do cho sự tồn tại của họ.

Video: https://www.youtube.com/watch?v=6mTbuzafcII


8

Tôi thấy việc đọc các ví dụ từ bộ chuyển đổi-js giúp tôi hiểu chúng một cách cụ thể về cách tôi có thể sử dụng chúng trong mã hàng ngày.

Ví dụ, hãy xem xét ví dụ này (lấy từ README tại liên kết ở trên):

var t = require("transducers-js");

var map    = t.map,
    filter = t.filter,
    comp   = t.comp,
    into   = t.into;

var inc    = function(n) { return n + 1; };
var isEven = function(n) { return n % 2 == 0; };
var xf     = comp(map(inc), filter(isEven));

console.log(into([], xf, [0,1,2,3,4])); // [2,4]

Đối với một, sử dụng xftrông gọn gàng hơn nhiều so với cách thay thế thông thường với Dấu gạch dưới.

_.filter(_.map([0, 1, 2, 3, 4], inc), isEven);

Làm thế nào mà ví dụ đầu dò dài hơn nhiều như vậy. Phiên bản gạch trông ngắn gọn hơn rất nhiều
appshare.co

1
@Zubair Không thực sựt.into([], t.comp(t.map(inc), t.filter(isEven)), [0,1,2,3,4])
Juan Castañeda

7

Bộ biến đổi là (theo hiểu biết của tôi!) Các chức năng nhận một chức năng giảm và trả về một chức năng khác. Một chức năng giảm là một trong đó

Ví dụ:

user> (def my-transducer (comp count filter))
#'user/my-transducer
user> (my-transducer even? [0 1 2 3 4 5 6])
4
user> (my-transducer #(< 3 %) [0 1 2 3 4 5 6])
3

Trong trường hợp này, đầu dò của tôi có chức năng lọc đầu vào mà nó áp dụng cho 0 sau đó nếu giá trị đó là chẵn? trong trường hợp đầu tiên bộ lọc chuyển giá trị đó đến bộ đếm, sau đó nó lọc giá trị tiếp theo. Thay vì lọc đầu tiên và sau đó chuyển tất cả các giá trị đó sang đếm.

Điều tương tự trong ví dụ thứ hai, nó kiểm tra từng giá trị một và nếu giá trị đó nhỏ hơn 3 thì nó sẽ cho phép đếm thêm 1.


Tôi thích lời giải thích đơn giản này
Ignacio

7

Một định nghĩa rõ ràng về bộ chuyển đổi là ở đây:

Transducers are a powerful and composable way to build algorithmic transformations that you can reuse in many contexts, and they’re coming to Clojure core and core.async.

Để hiểu nó, hãy xem xét ví dụ đơn giản sau:

;; The Families in the Village

(def village
  [{:home :north :family "smith" :name "sue" :age 37 :sex :f :role :parent}
   {:home :north :family "smith" :name "stan" :age 35 :sex :m :role :parent}
   {:home :north :family "smith" :name "simon" :age 7 :sex :m :role :child}
   {:home :north :family "smith" :name "sadie" :age 5 :sex :f :role :child}

   {:home :south :family "jones" :name "jill" :age 45 :sex :f :role :parent}
   {:home :south :family "jones" :name "jeff" :age 45 :sex :m :role :parent}
   {:home :south :family "jones" :name "jackie" :age 19 :sex :f :role :child}
   {:home :south :family "jones" :name "jason" :age 16 :sex :f :role :child}
   {:home :south :family "jones" :name "june" :age 14 :sex :f :role :child}

   {:home :west :family "brown" :name "billie" :age 55 :sex :f :role :parent}
   {:home :west :family "brown" :name "brian" :age 23 :sex :m :role :child}
   {:home :west :family "brown" :name "bettie" :age 29 :sex :f :role :child}

   {:home :east :family "williams" :name "walter" :age 23 :sex :m :role :parent}
   {:home :east :family "williams" :name "wanda" :age 3 :sex :f :role :child}])

Còn về việc chúng ta muốn biết có bao nhiêu trẻ em trong làng? Chúng ta có thể dễ dàng tìm ra nó với bộ giảm tốc sau:

;; Example 1a - using a reducer to add up all the mapped values

(def ex1a-map-children-to-value-1 (r/map #(if (= :child (:role %)) 1 0)))

(r/reduce + 0 (ex1a-map-children-to-value-1 village))
;;=>
8

Đây là một cách khác để làm điều đó:

;; Example 1b - using a transducer to add up all the mapped values

;; create the transducers using the new arity for map that
;; takes just the function, no collection

(def ex1b-map-children-to-value-1 (map #(if (= :child (:role %)) 1 0)))

;; now use transduce (c.f r/reduce) with the transducer to get the answer 
(transduce ex1b-map-children-to-value-1 + 0 village)
;;=>
8

Bên cạnh đó, nó cũng thực sự mạnh mẽ khi tính đến các nhóm con. Ví dụ, nếu chúng ta muốn biết có bao nhiêu trẻ em trong Brown Family, chúng ta có thể thực hiện:

;; Example 2a - using a reducer to count the children in the Brown family

;; create the reducer to select members of the Brown family
(def ex2a-select-brown-family (r/filter #(= "brown" (string/lower-case (:family %)))))

;; compose a composite function to select the Brown family and map children to 1
(def ex2a-count-brown-family-children (comp ex1a-map-children-to-value-1 ex2a-select-brown-family))

;; reduce to add up all the Brown children
(r/reduce + 0 (ex2a-count-brown-family-children village))
;;=>
2

Tôi hy vọng bạn có thể tìm thấy những ví dụ hữu ích. Bạn có thể tìm thêm ở đây

Hy vọng nó giúp.

Clemencio Morales Lucas.


3
"Bộ chuyển đổi là một cách mạnh mẽ và có thể kết hợp để xây dựng các phép biến đổi thuật toán mà bạn có thể sử dụng lại trong nhiều ngữ cảnh và chúng đang đến với Clojure core và core.async." định nghĩa có thể áp dụng cho hầu hết mọi thứ?
appshare.co

1
Tôi sẽ nói với hầu hết mọi bộ chuyển đổi Clojure.
Clemencio Morales Lucas

6
Nó giống như một tuyên bố sứ mệnh hơn là một định nghĩa.
Mars

4

Tôi đã viết blog về điều này với một ví dụ clojurescript giải thích cách các hàm tuần tự hiện có thể mở rộng bằng cách có thể thay thế hàm giảm.

Đây là điểm của đầu dò khi tôi đọc nó. Nếu bạn nghĩ về thao tác conshoặc conjthao tác được mã hóa cứng trong các thao tác như map, filterv.v., thì không thể truy cập được hàm giảm.

Với bộ chuyển đổi, hàm giảm được tách ra và tôi có thể thay thế nó như tôi đã làm với mảng javascript gốc pushnhờ vào bộ chuyển đổi.

(transduce (filter #(not (.hasOwnProperty prevChildMapping %))) (.-push #js[]) #js [] nextKeys)

filter và bạn bè có 1 hoạt động hiếm mới sẽ trả về một chức năng chuyển đổi mà bạn có thể sử dụng để cung cấp chức năng giảm của riêng bạn.


4

Đây là câu trả lời (chủ yếu) về biệt ngữ và mã miễn phí của tôi.

Hãy nghĩ về dữ liệu theo hai cách, một luồng (các giá trị xảy ra theo thời gian chẳng hạn như các sự kiện) hoặc một cấu trúc (dữ liệu tồn tại tại một thời điểm như danh sách, vectơ, mảng, v.v.).

Có một số hoạt động nhất định mà bạn có thể muốn thực hiện trên các luồng hoặc cấu trúc. Một trong những hoạt động như vậy là ánh xạ. Một hàm ánh xạ có thể tăng từng mục dữ liệu (giả sử đó là một số) và bạn có thể hy vọng hình dung cách điều này có thể áp dụng cho một luồng hoặc một cấu trúc.

Hàm ánh xạ chỉ là một trong những lớp hàm đôi khi được gọi là "hàm giảm". Một hàm giảm phổ biến khác là bộ lọc loại bỏ các giá trị khớp với một vị từ (ví dụ: loại bỏ tất cả các giá trị chẵn).

Bộ biến đổi cho phép bạn "gói" một chuỗi gồm một hoặc nhiều hàm giảm và tạo ra một "gói" (bản thân nó là một hàm) hoạt động trên cả hai luồng hoặc cấu trúc. Ví dụ: bạn có thể "đóng gói" một chuỗi các hàm giảm (ví dụ: lọc các số chẵn, sau đó ánh xạ các số kết quả để tăng chúng lên 1) và sau đó sử dụng "gói" bộ chuyển đổi đó trên một luồng hoặc cấu trúc giá trị (hoặc cả hai) .

Vậy điều này có gì đặc biệt? Thông thường, giảm các chức năng không thể được cấu tạo hiệu quả để hoạt động trên cả luồng và cấu trúc.

Vì vậy, lợi ích cho bạn là bạn có thể tận dụng kiến ​​thức của mình về các hàm này và áp dụng chúng vào nhiều trường hợp sử dụng hơn. Cái giá phải trả là bạn phải học thêm một số máy móc (tức là bộ chuyển đổi) để cung cấp thêm cho bạn sức mạnh này.


2

Theo như tôi hiểu, chúng giống như các khối xây dựng , được tách rời khỏi việc triển khai đầu vào và đầu ra. Bạn chỉ cần xác định hoạt động.

Vì việc triển khai hoạt động không có trong mã của đầu vào và không được thực hiện gì với đầu ra, nên các đầu dò có thể tái sử dụng rất nhiều. Chúng khiến tôi nhớ đến Flow s trong Akka Streams .

Tôi cũng mới sử dụng đầu dò, xin lỗi vì câu trả lời có thể không rõ ràng.


1

Tôi thấy bài đăng này cung cấp cho bạn một cái nhìn rõ hơn về đầu dò.

https://medium.com/@roman01la/und hieu-transducers-in-javascript-3500d3bd9624


3
Không khuyến khích các câu trả lời chỉ dựa vào các liên kết bên ngoài vì SO vì các liên kết có thể bị đứt bất cứ lúc nào trong tương lai. Trích dẫn nội dung trong câu trả lời của bạn để thay thế.
Vincent Cantin

@VincentCantin Trên thực tế, bài đăng trên Medium đã bị xóa.
Dmitri Zaitsev

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.