Apa yang dimaksud dengan "mengangkat" di Haskell?

140

Saya tidak mengerti apa itu "mengangkat". Haruskah saya terlebih dahulu memahami monads sebelum memahami apa itu "lift"? (Saya juga benar-benar tidak tahu tentang monad :) Atau dapatkah seseorang menjelaskannya kepada saya dengan kata-kata sederhana?

GabiMe
sumber
10
Mungkin berguna, mungkin tidak: haskell.org/haskellwiki/Lifting
kennytm

Jawaban:

184

Mengangkat lebih merupakan pola desain daripada konsep matematika (meskipun saya berharap seseorang di sekitar sini sekarang akan membantah saya dengan menunjukkan bagaimana lift adalah sebuah kategori atau sesuatu).

Biasanya Anda memiliki beberapa tipe data dengan parameter. Sesuatu seperti

data Foo a = Foo { ...stuff here ...}

Misalkan Anda menemukan banyak penggunaan Footipe numerik take ( Int, Doubledll) dan Anda tetap harus menulis kode yang membuka bungkus angka-angka ini, menambah atau mengalikannya, dan kemudian membungkusnya kembali. Anda dapat melakukan hubungan pendek ini dengan menulis kode unwrap-and-wrap sekali. Fungsi ini secara tradisional disebut "lift" karena terlihat seperti ini:

liftFoo2 :: (a -> b -> c) -> Foo a -> Foo b -> Foo c

Dengan kata lain, Anda memiliki fungsi yang mengambil fungsi dua argumen (seperti (+)operator) dan mengubahnya menjadi fungsi yang setara untuk Foos.

Jadi sekarang Anda bisa menulis

addFoo = liftFoo2 (+)

Edit: informasi lebih lanjut

Anda tentu saja bisa memiliki liftFoo3, liftFoo4dan seterusnya. Namun hal ini seringkali tidak diperlukan.

Mulailah dengan observasi

liftFoo1 :: (a -> b) -> Foo a -> Foo b

Tapi itu sama persis dengan fmap. Jadi daripada liftFoo1Anda akan menulis

instance Functor Foo where
   fmap f foo = ...

Jika Anda benar-benar ingin keteraturan lengkap Anda dapat mengatakan

liftFoo1 = fmap

Jika Anda bisa menjadikannya Foofunctor, mungkin Anda bisa menjadikannya sebagai functor aplikatif. Faktanya, jika Anda dapat menulis liftFoo2maka contoh aplikatif terlihat seperti ini:

import Control.Applicative

instance Applicative Foo where
   pure x = Foo $ ...   -- Wrap 'x' inside a Foo.
   (<*>) = liftFoo2 ($)

The (<*>)Operator untuk Foo memiliki jenis

(<*>) :: Foo (a -> b) -> Foo a -> Foo b

Ini menerapkan fungsi dibungkus ke nilai yang dibungkus. Jadi jika Anda dapat menerapkan liftFoo2maka Anda dapat menulis ini dalam istilah itu. Atau Anda bisa mengimplementasikannya secara langsung dan tidak repot liftFoo2, karena Control.Applicativetermasuk modul

liftA2 :: Applicative f => (a -> b -> c) -> f a -> f b -> f c

dan juga ada liftAdan liftA3. Tetapi Anda sebenarnya tidak terlalu sering menggunakannya karena ada operator lain

(<$>) = fmap

Ini memungkinkan Anda menulis:

result = myFunction <$> arg1 <*> arg2 <*> arg3 <*> arg4

Istilah myFunction <$> arg1mengembalikan fungsi baru yang dibungkus dengan Foo. Ini pada gilirannya dapat diterapkan ke argumen berikutnya menggunakan (<*>), dan seterusnya. Jadi, sekarang alih-alih memiliki fungsi lift untuk setiap wilayah, Anda hanya memiliki rangkaian aplikasi daisy chain.

Paul Johnson
sumber
26
Mungkin perlu diingat bahwa lift harus menghormati hukum standar lift id == iddan lift (f . g) == (lift f) . (lift g).
Carlos Scheidegger
14
Lift memang "kategori atau sesuatu". Carlos baru saja membuat daftar hukum Functor, di mana iddan .merupakan komposisi panah dan panah identitas dari beberapa kategori. Biasanya ketika berbicara tentang Haskell, kategori yang dimaksud adalah "Hask", yang panah yang Haskell fungsi (dengan kata lain, iddan .mengacu pada fungsi Haskell Anda kenal dan cinta).
Dan Burton
3
Ini harus dibaca instance Functor Foo, bukan instance Foo Functor? Saya akan mengedit diri saya sendiri tetapi saya tidak 100% yakin.
amalloy
2
Mengangkat tanpa Aplikatif adalah = Functor. Maksud saya, Anda memiliki 2 pilihan: Functor atau Applicative Functor. Pertama mengangkat fungsi parameter tunggal dan fungsi multi parameter kedua. Cukup banyak itu. Baik? Ini bukan ilmu roket :) hanya terdengar seperti itu. Terima kasih atas jawaban yang bagus btw!
jhegedus
2
@atc: ini adalah aplikasi parsial. Lihat wiki.haskell.org/P Partial_application
Paul Johnson
42

Paul dan yairchu adalah penjelasan yang bagus.

Saya ingin menambahkan bahwa fungsi yang diangkat dapat memiliki sejumlah argumen yang berubah-ubah dan tidak harus memiliki tipe yang sama. Misalnya, Anda juga dapat menentukan liftFoo1:

liftFoo1 :: (a -> b) -> Foo a -> Foo b

Secara umum, pengangkatan fungsi yang mengambil 1 argumen ditangkap di kelas tipe Functor, dan operasi pengangkatan disebut fmap:

fmap :: Functor f => (a -> b) -> f a -> f b

Perhatikan kemiripannya dengan liftFoo1tipe. Bahkan, jika sudah liftFoo1, Anda bisa membuat Foocontoh Functor:

instance Functor Foo where
  fmap = liftFoo1

Selanjutnya, generalisasi pengangkatan ke sejumlah argumen yang berubah-ubah disebut gaya aplikatif . Jangan repot-repot mendalami ini sampai Anda memahami pengangkatan fungsi dengan sejumlah argumen tetap. Tetapi ketika Anda melakukannya, Learn you a Haskell memiliki bab yang bagus tentang ini. The Typeclassopedia adalah dokumen bagus lainnya yang menjelaskan Functor dan Applicative (serta kelas tipe lainnya; gulir ke bawah ke bab kanan dalam dokumen itu).

Semoga ini membantu!

Martijn
sumber
25

Mari kita mulai dengan sebuah contoh (beberapa ruang putih ditambahkan untuk presentasi yang lebih jelas):

> import Control.Applicative
> replicate 3 'a'
"aaa"
> :t replicate
replicate        ::         Int -> b -> [b]
> :t liftA2
liftA2 :: (Applicative f) => (a -> b -> c) -> (f a -> f b -> f c)
> :t liftA2 replicate
liftA2 replicate :: (Applicative f) =>       f Int -> f b -> f [b]
> (liftA2 replicate) [1,2,3] ['a','b','c']
["a","b","c","aa","bb","cc","aaa","bbb","ccc"]
> ['a','b','c']
"abc"

liftA2mengubah fungsi tipe biasa menjadi fungsi tipe yang sama yang dibungkus dalamApplicative , seperti daftar IO, dll.

Lift umum lainnya adalah liftdari Control.Monad.Trans. Ini mengubah aksi monad dari satu monad menjadi aksi monad yang ditransformasikan.

Secara umum, "lift" mengangkat fungsi / tindakan menjadi tipe "terbungkus" (sehingga fungsi asli bekerja "di bawah pembungkus").

Cara terbaik untuk memahami ini, dan monad dll, dan untuk memahami mengapa mereka berguna, mungkin dengan membuat kode dan menggunakannya. Jika ada sesuatu yang Anda kodekan sebelumnya yang Anda curigai bisa mendapatkan keuntungan dari ini (yaitu ini akan membuat kode itu lebih pendek, dll.), Coba saja dan Anda akan dengan mudah memahami konsepnya.

yairchu
sumber
14

Mengangkat adalah konsep yang memungkinkan Anda mengubah fungsi menjadi fungsi terkait dalam pengaturan lain (biasanya lebih umum)

lihat di http://haskell.org/haskellwiki/Lifting

Nasser Hadjloo
sumber
40
Ya, tapi halaman itu dimulai dengan "Kami biasanya memulai dengan sebuah fungsi (kovarian) ...". Tidak benar-benar ramah pemula.
Paul Johnson
3
Tetapi "functor" sudah terhubung, jadi pemula cukup mengkliknya untuk melihat apa itu Functor. Harus diakui, halaman yang ditautkan tidak begitu bagus. Saya perlu mendapatkan akun dan memperbaikinya.
jrockway
10
Ini masalah yang pernah saya lihat di situs pemrograman fungsional lainnya; setiap konsep dijelaskan dalam istilah konsep lain (asing) sampai pemula menjadi lingkaran penuh (dan berputar-putar). Pasti ada hubungannya dengan menyukai rekursi.
DNA
2
Beri suara untuk tautan ini. Lift membuat koneksi antara satu dunia dan dunia lain.
eccstartup
4
Jawaban seperti ini hanya bagus jika Anda sudah memahami topiknya.
doubleOrt
-2

Menurut tutorial mengkilap ini , sebuah functor adalah suatu wadah (seperti Maybe<a>, List<a>atau Tree<a>yang dapat menyimpan elemen dari jenis lain, a). Saya telah menggunakan notasi umum Java <a>,, untuk tipe elemen adan menganggap elemen sebagai beri di pohon Tree<a>. Ada fungsi fmap, yang mengambil fungsi konversi elemen, a->bdan wadah functor<a>. Ini berlaku a->buntuk setiap elemen wadah yang secara efektif mengubahnya menjadi functor<b>. Ketika hanya argumen pertama yang diberikan a->b,, fmaptunggu functor<a>. Artinya, memasok a->bsaja mengubah fungsi level elemen ini menjadi fungsi functor<a> -> functor<b>yang beroperasi di atas container. Ini disebut pengangkatandari fungsi tersebut. Karena wadah juga disebut Functor , Functors daripada Monad merupakan prasyarat untuk pengangkatan. Monad adalah semacam "paralel" dengan pengangkatan. Keduanya mengandalkan pengertian Functor dan do f<a> -> f<b>. Perbedaannya adalah bahwa pengangkatan digunakan a->buntuk konversi sedangkan Monad mengharuskan pengguna untuk menentukan a -> f<b>.

Val
sumber
5
Saya memberi Anda nilai lebih rendah, karena "sebuah functor adalah suatu wadah" adalah umpan api rasa troll. Contoh: fungsi dari beberapa rke tipe (mari kita gunakan cuntuk variasi), adalah Functor. Mereka tidak "mengandung" apapun c. Dalam contoh ini, fmap adalah komposisi fungsi, mengambil satu a -> bfungsi dan r -> asatu, memberi Anda r -> bfungsi baru. Masih tidak ada wadah. Juga, jika saya bisa, saya akan menandainya lagi untuk kalimat terakhir.
BMeph
1
Juga, fmapadalah sebuah fungsi, dan tidak "menunggu" untuk apapun; "Wadah" menjadi Functor adalah inti dari pengangkatan. Selain itu, Monad, jika ada, adalah gagasan ganda untuk mengangkat: Monad memungkinkan Anda menggunakan sesuatu yang telah diangkat beberapa kali, seolah-olah hanya diangkat sekali - ini lebih dikenal sebagai perataan .
BMeph
1
@BMeph To wait, to expect, to anticipateadalah sinonim. Dengan mengatakan "fungsi menunggu" yang saya maksud adalah "fungsi mengantisipasi".
Val
@BMeph Saya akan mengatakan bahwa alih-alih memikirkan fungsi sebagai counterexample dengan gagasan bahwa functors adalah wadah, Anda harus memikirkan contoh fungsi fungsi yang waras sebagai contoh balasan untuk gagasan bahwa fungsi bukan wadah. Fungsi adalah pemetaan dari domain ke codomain, domain menjadi produk silang dari semua parameter, codomain menjadi tipe keluaran dari fungsi. Dengan cara yang sama daftar adalah pemetaan dari Naturals ke tipe bagian dalam dari daftar (domain -> codomain). Mereka menjadi lebih mirip jika Anda mengingat fungsinya atau tidak menyimpan daftarnya.
titik koma
@BMeph satu-satunya alasan daftar dianggap lebih seperti wadah adalah karena dalam banyak bahasa mereka dapat dimutasi, sedangkan fungsi tradisional tidak bisa. Tetapi di Haskell bahkan itu bukanlah pernyataan yang adil karena keduanya tidak dapat dimutasi, dan keduanya dapat dimutasi-salinan: b = 5 : adan f 0 = 55 f n = g n, keduanya melibatkan mutasi semu "wadah". Juga fakta bahwa daftar biasanya disimpan sepenuhnya dalam memori sedangkan fungsi biasanya disimpan sebagai penghitungan. Tapi daftar memoizing / monorphic yang tidak disimpan di antara panggilan keduanya memecahkan omong kosong dari ide itu.
titik koma