Applicative Functor, Bagian I
Bacaan tambahan:
- Applicative Functors dari Learn You a Haskell
- Typeclassopedia
Motivasi
Perhatikan tipe Employee
berikut:
type Name = String
data Employee = Employee { name :: Name
phone :: String }
,deriving Show
Tentunya konstruktor Employee
bertipe
Employee :: Name -> String -> Employee
Jika kita memiliki sebuah Name
dan sebuah String
, kita bisa terapkan
(apply) konstruktor Employee
untuk mendapatkan sebuah Employee
.
Misalkan kita tidak memiliki sebuah Name
dan String
, melainkan
Maybe Name
dan Maybe String
. Mungkin karena kita mendapatkannya dengan
melakukan parsing berkas yang penuh error, atau dari form yang tidak
sepenuhnya diisi, atau kasus-kasus lainnya. Mungkin kita tidak bisa membuat
Employee
, tapi paling tidak kita bisa membuat Maybe Employee
.
Kita akan mengubah fungsi (Name -> String -> Employee)
menjadi fungsi
(Maybe Name -> Maybe String -> Maybe Employee)
. Bisakah kita membuat
sesuatu bertipe seperti ini?
Name -> String -> Employee) ->
(Maybe Name -> Maybe String -> Maybe Employee) (
Tentu saja bisa. Saya pun yakin kalian sudah bisa membuatnya sambil tidur
sekarang. Kita bisa membayangkan bagaiman fungsi tersebut bekerja. Jika
salah satu dari name atau string berupa Nothing
, kita mendapatkan
Nothing
. Jika keduanya berupa Just
, kita mendapatkan Employee
yang
dibuat dengan konstruktor Employee
(terbungkus dengan Just
). Mari
kita lanjutkan…
Sekarang begini: bukannya kita memiliki sebuah Name
dan String
, namun
kita punya [Name]
dan [String]
. Mungkin kita bisa mendapatkan [Employee]
di sini? Sekarang kita mau
Name -> String -> Employee) ->
(Name] -> [String] -> [Employee]) ([
Kita bisa bayangkan dua cara untuk ini:
- Kita bisa memasangkan satu
Name
untuk satuString
untuk membuatEmployee
- Kita bisa memasangkan
Name
danString
dengan segala kombinasi kemungkinannya.
Atau bagaimana kalau begini: kita punya (e -> Name)
dan (e -> String)
untuk e
apapun. Sebagai contoh, e
mungkin sebuah struktur data yang
besar dan kita punya fungsi untuk mengekstrak Name
dan String
darinya.
Bisakah kita membuatnya menjadi (e -> Employee)
, yang merupakan resep
untuk mengekstrak Employee
dari struktur tersebut?
Name -> String -> Employee) ->
(-> Name) -> (e -> String) -> (e -> Employee)) ((e
Tidak masalah, dan kali ini hanya ada satu cara untuk menulis fungsi tersebut.
Generalisir
Setelah melihat kegunaan pola seperti di atas, mari kita sedikit menggeneralisir. Tipe fungsi yang kita inginkan adalah seperti berikut:
-> b -> c) -> (f a -> f b -> f c) (a
Hmm, terlihat familiar… serupa dengan tipe dari fmap
!
fmap :: (a -> b) -> (f a -> f b)
Satu-satunya perbedaan adalah sebuah argumen tambahan. Kita bisa menyebut
fungsi baru ini sebagai fmap2
, karena menerima sebuah fungsi dengan dua
argumen. Mungkin kita bisa menuliskannya dalam bentuk fmap
, sehingga kita
hanya memerlukan constraint Functor
pada f
:
fmap2 :: Functor f => (a -> b -> c) -> (f a -> f b -> f c)
= undefined fmap2 h fa fb
Setelah mencoba, Functor
tidak cukup membantu kita untuk membuat fmap2
.
Apa yang salah? Kita memiliki
h :: a -> b -> c
fa :: f a
fb :: f b
Perhatikan bahwa kita bisa menuliskan tipe h
sebagai (a -> (b -> c))
.
Jadi kita memiliki sebuah fungsi yang menerima a
, dan sebuah nilai bertipe
f a
. Kita tinggal “mengangkat” fungsi tersebut melewati f
dengan fmap
yang akan menghasilkan:
h :: a -> (b -> c)
fmap h :: f a -> f (b -> c)
fmap h fa :: f (b -> c)
Oke, sekarang kita memiliki sesuatu bertipe f (b -> c)
dan f b
…
dan di sinilah kita stuck! fmap
tidak bisa membantu lebih jauh.
fmap
memberikan cara untuk menerapkan fungsi ke nilai-nilai yang berada
di dalam konteks Functor
, tapi yang kita butuhkan sekarang adalah
penerapan fungsi yang juga berada di dalam konteks Functor
ke nilai-nilai
yang berada di konteks Functor
.
Applicative
Functor yang memiliki karakter seperti di atas (penerapan fungsi
berdasarkan konteks, contextual application) disebut applicative.
Kelas Applicative
(didefinisikan di [Control.Applicative
]
(http://haskell.org/ghc/docs/latest/html/libraries/base/Control-Applicative.html))
berpola seperti berikut ini.
class Functor f => Applicative f where
pure :: a -> f a
(<*>) :: f (a -> b) -> f a -> f b
Operator (<*>)
(biasa disebut “ap”, versi singkat dari apply,
terjemahan: terap) mewakili prinsip penerapan kontekstual
(contextual application). Perhatikan bahwa kelas Applicative
mewajibkan anggotanya untuk juga menjadi anggota Functor
, sehingga
kita selalu bisa menggunakan fmap
terhadap anggota Applicative
.
Applicative
juga memiliki method lain bernama pure
yang
memungkinkan kita untuk memasukkan nilai a
ke sebuah container.
Untuk saat ini, kita bisa bisa menyebut pure
sebagai fmap0
:
pure :: a -> f a
fmap :: (a -> b) -> f a -> f b
fmap2 :: (a -> b -> c) -> f a -> f b -> f c
Setelah kita memiliki (<*>)
, kita bisa mengimplemen fmap2
, yang
disebut liftA2
di pustaka standar:
liftA2 :: Applicative f => (a -> b -> c) -> f a -> f b -> f c
= (h `fmap` fa) <*> fb liftA2 h fa fb
Bahkan, pola ini cukup umum sehingga Control.Applicative
mendefinisikan
(<$>)
sebagai sinonim untuk fmap
,
(<$>) :: Functor f => (a -> b) -> f a -> f b
<$>) = fmap (
sehingga kita bisa menulis
= h <$> fa <*> fb liftA2 h fa fb
Bagaimana dengan liftA3
?
liftA3 :: Applicative f => (a -> b -> c -> d) -> f a -> f b -> f c -> f d
= ((h <$> fa) <*> fb) <*> fc liftA3 h fa fb fc
(Perhatikan bahwa prioritas dan sifat asosiatif dari (<$>)
dan (<*>)
didefinisikan sedemikian rupa sehingga semua tanda kurung di atas menjadi
tidak diperlukan.)
Ringkas! Tidak seperti perpindahan dari fmap
ke liftA2
(yang
membutuhkan generalisasi dari Functor
ke Applicative
), dari liftA2
ke
liftA3
(dan dari situ ke liftA4
, … dan seterusnya) tidak memerlukan
usaha tambahan. Applicative
sudah cukup.
Sebenarnya, ketika kita memiliki semua argumen, kita tidak perlu untuk
menyebutnya liftA2
, liftA3
, dan seterusnya. Cukup gunakan pola f <$> x <*> y <*> z <*> ...
langsung. (liftA2
dan lainnya berguna ketika saat
aplikasi parsial.)
Bagaimana dengan pure
? pure
digunakan ketika kita ingin menerapkan
sebuah fungsi ke beberapa argumen yang berada di dalam konteks functor f
,
tetapi salah satu argumennya tidak berada di dalam f
. Argumen tersebut bisa
disebut “pure” (murni). Kita bisa menggunakan pure
untuk mengangkat mereka
ke f
sebelum melakukan penerapan. Sebagai contoh:
liftX :: Applicative f => (a -> b -> c -> d) -> f a -> b -> f c -> f d
= h <$> fa <*> pure b <*> fc liftX h fa b fc
Hukum-hukum applicative
Hanya ada satu hukum yang benar-benar menarik untuk Applicative
:
`fmap` x === pure f <*> x f
Memetakan sebuah fungsi f
pada container x
harus memberikan hasil yang
sama dengan memasukkan fungsi tersebut ke container lalu menerapkannya ke x
dengan (<*>)
.
Ada hukum-hukum lainnya, tetapi mereka tidak begitu instruktif. Kalian bisa membacanya sendiri jika mau.
Contoh applicative
Maybe
Mari tulis beberapa anggota dari Applicative
, dimulai dengan Maybe
. pure
bekerja dengan memasukkan sebuah nilai ke bungkus Just
. (<*>)
adalah aplikasi/
penerapan fungsi dengan kemungkinan gagal, yang akan menghasilkan Nothing
jika
salah satu dari fungsi atau argumennya berupa Nothing
.
instance Applicative Maybe where
pure = Just
Nothing <*> _ = Nothing
<*> Nothing = Nothing
_ Just f <*> Just x = Just (f x)
Mari lihat contohnya:
m_name2 :: Maybe Name
m_name1,= Nothing
m_name1 = Just "Brent"
m_name2
m_phone2 :: Maybe String
m_phone1,= Nothing
m_phone1 = Just "555-1234"
m_phone2
= Employee <$> m_name1 <*> m_phone1
exA = Employee <$> m_name1 <*> m_phone2
exB = Employee <$> m_name2 <*> m_phone1
exC = Employee <$> m_name2 <*> m_phone2 exD