Haskell 這段代碼該如何理解?

isPalindrome = (==) &<*&> reverse

我能理解寫成這個樣子的

isPalindrome w = w == reverse w

我也知道

&<*&> = f (a -&> b) -&> f a -&> f b

但在這裡constructor好像不太容易看出來


確實這樣寫在通常的意義上可讀性不好, 不過既然題主想要一個直觀理解,

我覺得稍微解釋一下也還是可以達到覺得這個寫法比較直觀的.

來再一起看一遍題主給出的這段

isPalindrome = (==) &<*&> reverse

顯然最不好理解的是這裡的 &<*&> , 如果我們知道這裡的 &<*&> 對應的 Applicative的實例是哪個, 其實問題就解決一半了.

其實有經驗的話, 看到這裡沒有用到什麼特殊的構造, 那麼就應該往比較通用的Applicative的instance上考慮了, 查看文檔 http://hackage.haskell.org/package/base-4.9.0.0/docs/src/GHC.Base.html#Applicative 可以知道, 其實比較通用的Applicative的實例就2個, 分別是:

instance Applicative []
instance Applicative ((-&>) a)

我們再看一眼 &<*&> 的類型

(&<*&>) :: Applicative f =&> f (a -&> b) -&> f a -&> f b

那假如這裡用到的 Applicative 的 instance 是 [] 的話, &<*&> 的兩個參數就應該具有形如 [a] 的類型, 而 reverse 和 (==) 顯然不具有這樣的類型.

所以其實很容易知道這裡的 Applicative 的 instance 其實是 ((-&>) a).

至此問題已經解決一半了, 下面我們驗證一下.

把 f = (c -&>) (這裡把上面的a重命名成了c, 以及把 (-&>) 改寫成了中綴形式) 代入到 &<*&> 的類型得到specialize後的 &<*&> 的類型:

(&<*&>) :: (c -&> (a -&> b)) -&> (c -&> a) -&> (c -&> b)

又從 instance Applicative ((-&>) a) 中, 我們得到了 &<*&> 的實現

(&<*&>) f g x = f x (g x)

好了, 有了這些, 我們開始對原式做一些變形

isPalindrome = (==) &<*&> reverse -- (1) (原式)
isPalindrome = (&<*&>) (==) reverse -- (2) (中綴形式變為前綴形式)
isPalindrome xs = ((&<*&>) (==) reverse) xs -- (3) (逆eta變換)
isPalindrome xs = (&<*&>) (==) reverse xs -- (4) (函數應用的左結合性)
isPalindrome xs = (==) xs (reverse xs) -- (5) (代入 &<*&> 的實現)
isPalindrome xs = xs == (reverse xs) -- (6) (前綴形式變為中綴形式)

好了, 到了最後一步應該就是童叟無欺, 大家都看得懂啦, 誰還看不懂舉手, 我保證不叫家長..

到此問題已經解決, 不過肯定有同學要暴躁了: 說好的"直觀"理解呢...

好的, 所以下面要開始劃重點了哦:

當我們看到以下式子的時候

(&<*&>) :: f (a -&> b) -&> f a -&> f b

我們把式子中的 "f xxx" 替換成 "xxx的半成品" 然後念出來, 於是你就發現上式念作:

(a -&> b) 的半成品 -&> a的半成品 -&> b的半成品

以及, 你會發現 &<*&> 的作用其實就是, 當缺乏的材料被給足的時候, 先把 (a -&> b)的半成品 捏合成 (a -&> b) , 把 a的半成品 捏合成 a, 然後 把 (a -&> b) 應用於 a 得到 b, 然後返回b; 當然由於以上是假設缺乏的材料被給足的時候, 能返回b, 而目前還沒有達到這個假設, 所以上面的過程返回的仍然是 b的半成品. 用一句話來說: &<*&> 的作用就是對 一個半成品函數, 和一個半成品a, 這兩個東西進行組合, 然後返回一個半成品b.

於是當你看到下式的時候,

isPalindrome = (==) &<*&> reverse

很顯然, (==) 是 (a -&> b)的半成品, reverse 是 a的半成品.

那麼這時候其實主要問題就在於: 這裡的 "半成品" 體現在哪兒?

這裡的 "半成品" 體現在哪兒, 其實就看選用 Applicative的哪個實例了.

根據上面已經說過的思路, 我們很快意識到, 這裡應該用 (c -&>) 這個實例, 於是這時候就很清楚: "半成品" 的 "半成" 就體現在 c 還沒有被應用上去.

那麼 "成品" 應該長啥樣子呢? 當然就是 c 被應用上去之後的樣子了咯. 也就是 (==) 應用上 xs 變成 (xs ==) , 以及 reverse 應用上 xs 變成 (reverse xs); 然後對這兩者做一個函數應用, 就得到了

xs == (reverse xs) , 最後別忘了, 我們剛剛借了一個 xs, 還回去那麼就是

xs -&> xs == (reverse xs)

(注意上面的 (xs ==) 雖然是個對函數的部分應用, 但是相對於 (==) 這個半成品來說, 它已經完整了)

最後的好消息是, 以上方法對 Functor, Applicative, Monad 均有效.

關於上述思路用在Monad上的例子, 可以參考這篇回答 Python中實現 (a and b or c) in xx 這種邏輯最簡潔的方式是怎樣的? , (當然其實那個回答里也可以用 Applicative 的)

在Monad的do notation裡面的 a &<- a 的寫法, 其實就可以理解為, 我們有了 a 這個半成品, 然後通過借了某些東西, 得到了一個 a 這個成品, 於是在這個do notation後續的邏輯里, 就可以直接用 a 這個成品去玩耍啦.

最後回顧一下剛才這個普通到一般人看不見的 Function Applicative Monad 的實例:

所以對於這麼好玩的Haskell大法大家是不是應該多多茲糍捏?


啊,&<*&>是S Combinator,另外之前Ice還在CodeWars上找出了用(&>&>=)((&>&>=) === flip (&<*&>))寫的……

然後你使用的instance是((-&>) a):

啊!忘了,這個玩意是haddock的bug沒做α-conversion。上次 @大笨蛋千里冰封 提到和你提的同一個問題之後我發現的,然後社區現在還沒回應(Haddock generated type signature of instance methods did not do α-conversion · Issue #613 · haskell/haddock),哦,該死,我標題上還出了個語法錯…… Anyway,正確的應該是這樣的:

然後這個instance是:

instance Applicative ((-&>) a) where
pure = const
(&<*&>) f g x = f x (g x)

(為啥長或寬小於120px的圖片不能上傳?)

(哦,另外知乎的code highlighting改了?新的Haskell highlighter好好看……)

f x (g x)和x z (y z)。

然後你自己做下substitution:

(==) &<*&> reverse
==&> (x -&> f x (g x))[f --&> (==), g --&> reverse]
==&> (x -&> (==) x (reverse x))

(相信你知道(==)和reverse函數的作用)

那麼,這樣看就簡單多了,比較x和被反轉的x,就是palindrome的定義。

----

啊……原來highlighter只是在編輯器裡面換了啊……顯示出來還是原來的那種。


謝邀,既然 @霧雨魔理沙也來了,我也湊個熱鬧。

先上結論,這種方式寫代碼是很不好的,除了耗費腦細胞外,沒有其他好處。只有一種情況例外,就是這個代碼不是給人讀的時候,一般是直接給編譯器的由程序生成的代碼。

這個isPalindrome = (==) &<*&> reverse表達式所蘊含的函子是((-&>) r),也就是Reader函子,類型((-&>) r) a的意義是給一個類型r的輸入,將得到一個類型a的輸出。那如何理解這個表達式呢?

首先我們有

(==) :: [a] -&> [a] -&> Bool -- same as: [a] -&> ([a] -&> Bool)
reverse :: [a] -&> [a]

應用運算符&<*&>後有

(&<*&>) :: f (a -&> b) -&> f a -&> f b
(==) &<*&> reverse :: ([a] -&> ([a] -&> Bool)) -&> ([a] -&> [a]) -&> ([a] -&> Bool)
令 f = ((-&>) [a]) 則有 f ([a] -&> Bool) -&> f [a] -&> f Bool

這樣就很明顯了,因函子((-&>) [a]) 的輸入是[a],因此可以得到如下正常的寫法

((==) &<*&> reverse) w
-- By definition of &<*&> in instance Applicative ((-&>) r)
-- have f &<*&> g = x -&> f x (g x)
= ((==) w) (reverse w)
-- Transform prefix of (==) to infix
= w == reverse w

isPalindrome w = w == reverse w

這是最好的寫法,簡單、直觀又不費腦,強烈建議。


a -&> b 就是 (-&>) a b,也就是 ((-&>) a) b


我也來湊個熱鬧

首先 在 ghci 中查看

λ&> :i &<*&>
class Functor f =&> Applicative (f :: * -&> *) where
...
(&<*&>) :: f (a -&> b) -&> f a -&> f b
...
-- Defined in 『GHC.Base』
infixl 4 &<*&>

其中已看出 f 是一種 Applicative

然後查看

λ&> :i Applicative
class Functor f =&> Applicative (f :: * -&> *) where
pure :: a -&> f a
(&<*&>) :: f (a -&> b) -&> f a -&> f b
(*&>) :: f a -&> f b -&> f b
(&<*) :: f a -&> f b -&> f a
{-# MINIMAL pure, (&<*&>) #-}
-- Defined in 『GHC.Base』
instance Applicative (Either e) -- Defined in 『Data.Either』
instance Applicative [] -- Defined in 『GHC.Base』
instance Applicative Maybe -- Defined in 『GHC.Base』
instance Applicative IO -- Defined in 『GHC.Base』
instance Applicative ((-&>) a) -- Defined in 『GHC.Base』
instance Monoid a =&> Applicative ((,) a) -- Defined in 『GHC.Base』

其中發現 (-&>) 也是一種 Applicative,那麼 &<*&> 實際 的類型是

([a] -&> [a] -&> Bool) -&> ([a] -&> [a]) -&> ([a] -&> Bool)

其中 f 的等價內容是

([a] -&>)

a 等等價內容是

([a] -&> Bool)

b 的等價內容是

([a])

然後 (==) 與 reverse 的類型應該是眾所周知的吧。。

(是不是有人覺得有點亂,有人覺得再說吧)


(&<*&>) :: (c -&> (a -&> b)) -&> (c -&> a) -&> (c -&> b)
ff &<*&> fx = go where go x = ff x $ fx x

這麼理解

吐個槽,貌似Haskell的語法高亮除了GHC欽定的haddock好像都高亮的好奇怪


根據這個理解吧,我昨晚上在被Voile鄙視智商的時候,想出了一個類似的東西。

原函數是樹狀數組的Lowbit,OIer們肯定熟知的C++的寫法是:

template&

constexpr auto lowbit(T x) -&> T {

/*我是一個縮進*/return x (-x);

}

很簡單吧。 Haskell中樸素寫法完全一樣,不過用題主那個類似的思路可以寫成這樣:

f = (..) &<*&> (0-)

然而這並不能阻止Voile鄙視我智商


函數作為Applicative,有

f &<*&> g = x -&> f x (g x)

isPalindrome = x -&> (==) x (reverse x)


推薦閱讀:

設計師學編程?從五個思維訓練開始
getter 和 setter 方法有什麼意義?
我們為什麼需要React?
學習編程的好方法——控制台遊戲
git:一個本地分支可以對應多個遠程分支么?

TAG:編程 | 函數式編程 | Haskell |