Đơn nguyên miễn phí là gì?


368

Tôi đã nhìn thấy những hạn miễn phí Monad bật lên mỗi giờ sau đó một thời gian, nhưng tất cả mọi người chỉ dường như sử dụng / thảo luận mà không đưa ra một lời giải thích về những gì họ đang có. Vậy: các đơn nguyên miễn phí là gì? (Tôi muốn nói rằng tôi quen thuộc với các đơn nguyên và những điều cơ bản của Haskell, nhưng chỉ có kiến ​​thức rất sơ sài về lý thuyết thể loại.)


12
Một lời giải thích khá hay là ở đây haskellforall.com/2012/06/ Khăn
Roger Lindsjö

19
@Roger đó là loại trang đưa tôi đến đây. Đối với tôi, ví dụ đó định nghĩa một thể hiện đơn nguyên cho một loại có tên là "Miễn phí" và đó là nó.
David

Câu trả lời:


295

Câu trả lời của Edward Kmett rõ ràng là tuyệt vời. Nhưng, nó là một chút kỹ thuật. Đây là một lời giải thích có lẽ dễ tiếp cận hơn.

Các đơn vị tự do chỉ là một cách chung để biến functor thành các đơn vị. Đó là, cho bất kỳ functor f Free flà một đơn nguyên. Điều này sẽ không hữu ích lắm, ngoại trừ bạn có một cặp chức năng

liftFree :: Functor f => f a -> Free f a
foldFree :: Functor f => (f r -> r) -> Free f r -> r

cách thứ nhất trong số này cho phép bạn "vào" đơn nguyên của mình và cách thứ hai cho bạn cách "thoát" khỏi nó.

Tổng quát hơn, nếu X là Y có thêm một số thứ P, thì "X miễn phí" là cách để chuyển từ Y sang X mà không thu được thêm bất cứ điều gì.

Ví dụ: một monoid (X) là một tập hợp (Y) có cấu trúc bổ sung (P) về cơ bản nói rằng nó có một hoạt động (bạn có thể nghĩ về phép cộng) và một số nhận dạng (như số không).

Vì thế

class Monoid m where
   mempty  :: m
   mappend :: m -> m -> m

Bây giờ, tất cả chúng ta đều biết danh sách

data [a] = [] | a : [a]

Chà, với bất kỳ loại nào tchúng ta biết đó [t]là một monoid

instance Monoid [t] where
  mempty   = []
  mappend = (++)

và vì vậy các danh sách là "monoid miễn phí" trên các bộ (hoặc trong các loại Haskell).

Được rồi, vì vậy các đơn vị miễn phí là cùng một ý tưởng. Chúng tôi lấy một functor, và trả lại một đơn nguyên. Trong thực tế, vì các đơn nguyên có thể được xem là đơn sắc trong danh mục endofunctor, định nghĩa của một danh sách

data [a] = [] | a : [a]

trông rất giống định nghĩa của các đơn nguyên miễn phí

data Free f a = Pure a | Roll (f (Free f a))

Monadthể hiện có sự tương đồng với Monoidthể hiện cho các danh sách

--it needs to be a functor
instance Functor f => Functor (Free f) where
  fmap f (Pure a) = Pure (f a)
  fmap f (Roll x) = Roll (fmap (fmap f) x)

--this is the same thing as (++) basically
concatFree :: Functor f => Free f (Free f a) -> Free f a
concatFree (Pure x) = x
concatFree (Roll y) = Roll (fmap concatFree y)

instance Functor f => Monad (Free f) where
  return = Pure -- just like []
  x >>= f = concatFree (fmap f x)  --this is the standard concatMap definition of bind

Bây giờ, chúng tôi có hai hoạt động của chúng tôi

-- this is essentially the same as \x -> [x]
liftFree :: Functor f => f a -> Free f a
liftFree x = Roll (fmap Pure x)

-- this is essentially the same as folding a list
foldFree :: Functor f => (f r -> r) -> Free f r -> r
foldFree _ (Pure a) = a
foldFree f (Roll x) = f (fmap (foldFree f) x)

12
Đây có thể là lời giải thích tốt nhất về "miễn phí" mà tôi từng thấy. Đặc biệt là đoạn bắt đầu bằng "Nói chung hơn."
John L

16
Tôi nghĩ rằng thật thú vị khi nhìn vào Free f a = Pure a | Roll (f (Free f a))như Free f a = a + fa + ffa + ..., tức là "f áp dụng cho một bất kỳ số lần". Sau đó concatFree(tức là join) lấy "f áp dụng bất kỳ số lần nào cho (f áp dụng bất kỳ số lần nào cho a)" và thu gọn hai ứng dụng lồng nhau thành một. Và >>=lấy "f áp dụng bất kỳ số lần nào cho một" và "làm thế nào để đi từ a đến (b với f áp dụng bất kỳ số lần nào)", và về cơ bản áp dụng lần sau cho một lần bên trong trước và thu gọn lồng nhau. Bây giờ tôi tự nhận được nó!
jkff

1
concatFreevề cơ bản là joingì?
rgrinberg

11
Đây là một lời giải thích có lẽ dễ tiếp cận hơn. [Vượt] Trên thực tế, vì các đơn nguyên có thể được xem là đơn âm trong danh mục functor endo, nên ĐIith Tuy nhiên, tôi nghĩ rằng đây là một câu trả lời rất hay.
Ruud

2
"monads có thể được xem như monoids trong danh mục của endo functors" <3 (bạn nên liên kết đến stackoverflow.com/a/3870310/1306877 vì mỗi haskeller nên biết về tài liệu tham khảo mà!)
tlo

418

Đây là một câu trả lời thậm chí còn đơn giản hơn: Một Monad là thứ "tính toán" khi bối cảnh đơn nguyên bị sụp đổ bởi join :: m (m a) -> m a(nhớ lại >>=có thể được định nghĩa là x >>= y = join (fmap y x)). Đây là cách Monads thực hiện bối cảnh thông qua một chuỗi tính toán liên tiếp: bởi vì tại mỗi điểm trong chuỗi, bối cảnh từ cuộc gọi trước được thu gọn với lần tiếp theo.

Một đơn vị tự do đáp ứng tất cả các luật Monad, nhưng không thực hiện bất kỳ sự sụp đổ nào (tức là tính toán). Nó chỉ xây dựng một loạt các bối cảnh lồng nhau. Người dùng tạo ra một giá trị đơn âm miễn phí như vậy có trách nhiệm thực hiện một cái gì đó với các bối cảnh lồng nhau đó, để ý nghĩa của một tác phẩm như vậy có thể được hoãn lại cho đến khi giá trị đơn âm được tạo ra.


8
Đoạn văn của bạn thực sự là một bổ sung tuyệt vời cho bài viết của Philip.
David

20
Tôi thực sự thích câu trả lời này.
danidiaz

5
Đơn nguyên miễn phí có thể thay thế lớp loại Monad không? Đó là, tôi có thể viết chương trình chỉ bằng cách trả lại và ràng buộc của đơn nguyên miễn phí, sau đó tham gia kết quả bằng cách sử dụng Mwybe hoặc List hoặc bất cứ điều gì, hoặc thậm chí tạo ra nhiều chế độ xem đơn âm của một chuỗi các lệnh gọi hàm bị ràng buộc / được ẩn. Bỏ qua đáy và nontermination, đó là.
misterbee

2
Câu trả lời này có ích, nhưng tôi nghĩ nó sẽ làm tôi bối rối nếu tôi không gặp 'tham gia' trên khóa học của NicTA và đọc haskellforall.com/2012/06/ . Vì vậy, đối với tôi, mẹo để hiểu là đọc rất nhiều câu trả lời cho đến khi nó chìm vào. (Tài liệu tham khảo của NICTA: github.com/NICTA/cference/blob/master/src/Cference/Bind.hs )
Martin Capodici

1
câu trả lời này là tốt nhất từ ​​trước đến nay
Curycu

142

Một foo miễn phí xảy ra là điều đơn giản nhất thỏa mãn tất cả các luật 'foo'. Đó là để nói rằng nó đáp ứng chính xác các luật cần thiết để trở thành một foo và không có gì thêm.

Một functor hay quên là một phần "quên" một phần cấu trúc khi nó đi từ loại này sang loại khác.

Cho functor F : D -> C, và G : C -> D, chúng tôi nói F -| G, Fđược điều chỉnh bên trái G, hoặc Glà điều chỉnh bên phải cho Fbất cứ khi nào forall a, b: F a -> blà đẳng cấu với a -> G b, trong đó các mũi tên đến từ các loại thích hợp.

Chính thức, một functor miễn phí được điều chỉnh lại cho một functor hay quên.

Monoid miễn phí

Chúng ta hãy bắt đầu với một ví dụ đơn giản hơn, monoid miễn phí.

Lấy một monoid, được xác định bởi một số bộ mang T, hàm nhị phân để trộn một cặp phần tử với nhau f :: T → T → Tvà a unit :: T, sao cho bạn có luật kết hợp và luật định danh : f(unit,x) = x = f(x,unit).

Bạn có thể tạo ra một functor Utừ thể loại đơn sắc (trong đó mũi tên là đồng cấu đơn hình, nghĩa là chúng đảm bảo chúng ánh xạ unitsang unitđơn hình khác và bạn có thể soạn trước hoặc sau khi ánh xạ sang đơn hình khác mà không thay đổi ý nghĩa) trong số các bộ (trong đó mũi tên chỉ là mũi tên chức năng) 'quên' về hoạt động unitvà chỉ cung cấp cho bạn bộ vận chuyển.

Sau đó, bạn có thể định nghĩa một functor Ftừ danh mục các tập hợp trở lại danh mục các đơn âm được điều chỉnh trái với functor này. Functor đó là functor ánh xạ một tập hợp thành ađơn hình [a], ở đâu unit = []mappend = (++).

Vì vậy, để xem xét ví dụ của chúng tôi cho đến nay, trong pseudo-Haskell:

U : Mon  Set -- is our forgetful functor
U (a,mappend,mempty) = a

F : Set  Mon -- is our free functor
F a = ([a],(++),[])

Sau đó để hiển thị Flà miễn phí, chúng ta cần chứng minh rằng nó được điều chỉnh trái U, một functor hay quên, nghĩa là, như chúng ta đã đề cập ở trên, chúng ta cần chứng minh rằng

F a → b đẳng cấu a → U b

Bây giờ, hãy nhớ mục tiêu Flà trong danh mục Monđơn chất, trong đó mũi tên là đồng cấu đơn hình, vì vậy chúng ta cần phải chỉ ra rằng một phép đồng hình đơn hình từ [a] → bcó thể được mô tả chính xác bởi một hàm từ a → b.

Trong Haskell, chúng tôi gọi khía cạnh của cái này sống trong Set(er, Haskdanh mục các loại Haskell mà chúng tôi giả vờ là Set), chỉ foldMap, khi chuyên biệt từ Data.FoldableDanh sách có loại Monoid m => (a → m) → [a] → m.

Có những hậu quả xảy ra sau đây là một sự điều chỉnh. Đáng chú ý là nếu bạn quên thì hãy xây dựng miễn phí, sau đó quên lại, giống như bạn đã quên một lần và chúng ta có thể sử dụng điều này để xây dựng sự tham gia đơn nguyên. kể từ UFUF~U(FUF) ~ UF, và chúng ta có thể chuyển từ trạng thái đồng hình đơn hình nhận dạng từ [a]sang [a]qua cấu trúc đẳng cấu xác định điều chỉnh của chúng ta, nhận được rằng một danh sách đẳng cấu từ [a] → [a]là một hàm của kiểu a -> [a]và đây chỉ là trả về cho danh sách.

Bạn có thể soạn tất cả những điều này trực tiếp hơn bằng cách mô tả một danh sách theo các thuật ngữ này với:

newtype List a = List (forall b. Monoid b => (a -> b) -> b)

Đơn nguyên miễn phí

Vậy cái gì là Monad miễn phí là gì?

Vâng, chúng tôi làm điều tương tự như chúng tôi đã làm trước đây, chúng tôi bắt đầu với một functor U đáng quên từ thể loại đơn nguyên trong đó mũi tên là đồng cấu đơn nguyên đến một thể loại endofunctor trong đó mũi tên là biến đổi tự nhiên, và chúng tôi tìm kiếm một functor bị điều chỉnh trái đến đó

Vì vậy, làm thế nào điều này liên quan đến khái niệm của một đơn nguyên tự do như nó thường được sử dụng?

Biết rằng một cái gì đó là một đơn nguyên miễn phí Free f, nói với bạn rằng đưa ra một sự đồng hình đơn nguyên từFree f -> m , cũng giống như việc tạo ra một phép biến đổi tự nhiên (một phép đồng hình functor) từ f -> m. Hãy nhớ rằng F a -> bphải là đẳng cấu để a -> U bF được điều chỉnh trái với U. U ở đây ánh xạ các đơn nguyên cho functor.

F ít nhất là đẳng cấu với Freeloại tôi sử dụng trong freegói hackage.

Chúng tôi cũng có thể xây dựng nó tương tự chặt chẽ hơn với mã ở trên cho danh sách miễn phí, bằng cách xác định

class Algebra f x where
  phi :: f x -> x

newtype Free f a = Free (forall x. Algebra f x => (a -> x) -> x)

Cofree Comonads

Chúng ta có thể xây dựng một cái gì đó tương tự, bằng cách nhìn vào sự điều chỉnh đúng cho một functor hay quên giả sử nó tồn tại. Một functor cofree chỉ đơn giản là / sự điều chỉnh đúng / cho một functor hay quên, và bằng cách đối xứng, biết một cái gì đó là một comonad cofree cũng giống như biết rằng việc đưa ra một phép đồng hình comonad từ đó w -> Cofree fcũng giống như việc chuyển đổi tự nhiên w -> f.


12
@PauloScardine, đây không phải là điều bạn phải quan tâm. Câu hỏi của tôi xuất phát từ sự quan tâm để hiểu một số cấu trúc dữ liệu nâng cao và có thể hiểu sơ lược về những gì tiên tiến trong phát triển Haskell ngay bây giờ - không có cách nào cần thiết hoặc đại diện cho những gì thực sự viết Haskell cho đến nay. (Và hãy ngẩng cao đầu, mọi chuyện sẽ tốt hơn khi bạn vượt qua giai đoạn học IO một lần nữa)
David

8
@PauloScardine Bạn không cần câu trả lời ở trên để lập trình hiệu quả trong Haskell, ngay cả với các đơn nguyên miễn phí. Trên thực tế, tôi không khuyên bạn nên tấn công đơn nguyên miễn phí theo cách này cho người không có nền tảng về lý thuyết thể loại. Có rất nhiều cách để nói về nó từ góc độ hoạt động và tìm hiểu cách sử dụng mà không cần đi sâu vào lý thuyết thể loại. Tuy nhiên, tôi không thể trả lời câu hỏi về việc họ đến từ đâu mà không đi sâu vào lý thuyết. Các công trình miễn phí là một công cụ mạnh mẽ trong lý thuyết thể loại, nhưng bạn không cần nền tảng này để sử dụng chúng.
Edward KMett

18
@PauloScardine: Bạn chính xác không cần tính toán để sử dụng Haskell một cách hiệu quả và thậm chí hiểu những gì bạn đang làm. Có một chút kỳ lạ khi phàn nàn rằng "ngôn ngữ này là toán học" khi tính toán chỉ là một điều tốt đẹp mà bạn có thể sử dụng để giải trí và kiếm lợi nhuận. Bạn không có được những điều này trong hầu hết các ngôn ngữ bắt buộc. Tại sao bạn lại phàn nàn về tính năng bổ sung? Bạn chỉ có thể chọn KHÔNG lý luận về mặt toán học và tiếp cận nó như bất kỳ ngôn ngữ mới nào khác.
Sarah

3
@Sarah: Tôi vẫn chưa thấy một đoạn tài liệu hoặc cuộc trò chuyện IRC về haskell không nặng về lý thuyết máy tính và lambda tính toán nhiệt.
Paulo Scardine

11
@PauloScardine điều này trôi đi một chút OT, nhưng trong sự bảo vệ của Haskell: những điều kỹ thuật tương tự áp dụng cho tất cả các ngôn ngữ lập trình khác, chỉ có điều Haskell có một bản biên dịch hay đến mức mọi người thực sự có thể thích nói về chúng. Tại sao / làm thế nào X là một đơn vị thú vị đối với nhiều người, các cuộc thảo luận về tiêu chuẩn dấu phẩy động của IEEE không; cả hai trường hợp đều không quan trọng đối với hầu hết mọi người, vì họ chỉ có thể sử dụng kết quả.
David

72

Free Monad (cấu trúc dữ liệu) thuộc về Monad (lớp) như Danh sách (cấu trúc dữ liệu) cho Monoid (lớp): Đây là cách triển khai tầm thường, nơi bạn có thể quyết định sau đó cách kết hợp nội dung.


Bạn có thể biết những gì một đơn nguyên là gì và rằng mỗi đơn nguyên cần một cụ thể (Monad-tuân thủ pháp luật) thực hiện một trong hai fmap+join + returnhoặc bind+return .

Giả sử bạn có Functor (triển khai fmap ) nhưng phần còn lại phụ thuộc vào các giá trị và lựa chọn được thực hiện trong thời gian chạy, điều đó có nghĩa là bạn muốn có thể sử dụng các thuộc tính Monad nhưng sau đó muốn chọn các hàm Monad.

Điều đó có thể được thực hiện bằng cách sử dụng Free Monad (cấu trúc dữ liệu), bao bọc Functor (loại) theo cách sao cho join thay vì xếp chồng các hàm đó hơn là giảm.

Thực tế returnjoinbạn muốn sử dụng, giờ đây có thể được cung cấp dưới dạng tham số cho hàm rút gọn foldFree:

foldFree :: Functor f => (a -> b) -> (f b -> b) -> Free f a -> b
foldFree return join :: Monad m => Free m a -> m a

Để giải thích các loại, chúng ta có thể thay thế Functor fbằng Monad mbbằng (m a):

foldFree :: Monad m => (a -> (m a)) -> (m (m a) -> (m a)) -> Free m a -> (m a)

8
Câu trả lời này cho tôi ấn tượng rằng tôi hiểu những gì chúng thậm chí có thể hữu ích cho.
David

59

Một đơn nguyên miễn phí Haskell là một danh sách các functor. Đối chiếu:

data List a   = Nil    | Cons  a (List a  )

data Free f r = Pure r | Free (f (Free f r))

Purelà tương tự NilFreetương tự như Cons. Một đơn nguyên miễn phí lưu trữ một danh sách các hàm tử thay vì danh sách các giá trị. Về mặt kỹ thuật, bạn có thể triển khai các đơn nguyên miễn phí bằng cách sử dụng một loại dữ liệu khác nhau, nhưng bất kỳ triển khai nào cũng phải đồng nhất với kiểu trên.

Bạn sử dụng các đơn nguyên miễn phí bất cứ khi nào bạn cần một cây cú pháp trừu tượng. Functor cơ sở của đơn nguyên tự do là hình dạng của mỗi bước của cây cú pháp.

Bài đăng của tôi , mà ai đó đã liên kết, đưa ra một số ví dụ về cách xây dựng cây cú pháp trừu tượng với các đơn nguyên miễn phí


6
Tôi biết bạn chỉ đang vẽ một sự tương tự chứ không phải là một định nghĩa, nhưng một đơn vị tự do chắc chắn không giống với một danh sách các functor theo bất kỳ ý nghĩa nào. Nó gần gũi hơn với một cây functor.
Tom Ellis

6
Tôi đứng theo thuật ngữ của tôi. Ví dụ: bằng cách sử dụng gói chỉ mục lõi của tôi, bạn có thể định nghĩa "mức độ hiểu đơn nguyên miễn phí", hoạt động giống như danh sách đơn nguyên, ngoại trừ việc bạn liên kết hàm functor thay vì giá trị. Một đơn vị tự do là một danh sách các functor theo nghĩa là nếu bạn dịch tất cả các khái niệm Haskell lên danh mục của functor, thì các danh sách sẽ trở thành các đơn vị tự do. Một cây functor thực sự sau đó trở thành một cái gì đó hoàn toàn khác.
Gabriel Gonzalez

4
Bạn đã đúng rằng monad là sự phân loại, theo một cách nào đó, về khái niệm monoid, do đó các monad tự do tương tự như monoids tự do, tức là danh sách. Đến mức đó bạn chắc chắn đúng. Tuy nhiên, cấu trúc của một giá trị của một đơn nguyên miễn phí không phải là một danh sách. Nó là một cái cây, như tôi nói chi tiết dưới đây .
Tom Ellis

2
@TomEllis Về mặt kỹ thuật, nó chỉ là một cái cây nếu functor cơ sở của bạn là một functor sản phẩm. Khi bạn có một functor tổng làm functor cơ sở, nó gần giống với máy stack hơn.
Gabriel Gonzalez

21

Tôi nghĩ rằng một ví dụ cụ thể đơn giản sẽ giúp. Giả sử chúng ta có một functor

data F a = One a | Two a a | Two' a a | Three Int a a a

với sự rõ ràng fmap. Sau đó Free F alà loại cây có lá có loạia và có các nút được gắn thẻ với One, Two, Two'Three. One-nodes có một con, Two- và Two'-ode có hai con và Three-ode có ba và cũng được gắn thẻ với một Int.

Free Flà một đơn nguyên. returnánh xạ xtới cây chỉ là một chiếc lá có giá trị x. t >>= fnhìn vào từng chiếc lá và thay thế chúng bằng cây. Khi chiếc lá có giá trị, ynó thay thế chiếc lá đó bằng cây f y.

Một sơ đồ làm cho điều này rõ ràng hơn, nhưng tôi không có phương tiện để dễ dàng vẽ!


14
Những gì các bạn đang nói là các đơn vị tự do có hình dạng của functor. Vì vậy, nếu functor giống như cây (sản phẩm), đơn nguyên miễn phí giống như cây; nếu nó giống như danh sách (tổng), đơn nguyên miễn phí giống như danh sách; nếu nó giống như chức năng, đơn nguyên miễn phí giống như chức năng; vv Điều này có ý nghĩa với tôi. Vì vậy, giống như trong một monoid miễn phí, bạn tiếp tục coi mọi ứng dụng của mappend là tạo ra một yếu tố hoàn toàn mới; trong đơn nguyên miễn phí, bạn coi mọi ứng dụng của functor là một yếu tố hoàn toàn mới.
Bartosz Milewski

4
Ngay cả khi functor là "sum functor", đơn vị tự do kết quả vẫn giống như cây. Bạn kết thúc với nhiều loại nút trong cây của mình: một loại cho mỗi thành phần của tổng của bạn. Nếu "sum functor" của bạn là X -> 1 + X thì thực sự bạn có một danh sách, đó chỉ là một loại cây thoái hóa.
Tom Ellis
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.