这段混淆的 Haskell 代码是如何工作的?
Posted
技术标签:
【中文标题】这段混淆的 Haskell 代码是如何工作的?【英文标题】:How does this piece of obfuscated Haskell code work? 【发布时间】:2012-09-30 08:50:04 【问题描述】:在阅读https://en.uncyclopedia.co/wiki/Haskell(并忽略所有“冒犯性”内容)时,我偶然发现了以下一段混淆代码:
fix$(<$>)<$>(:)<*>((<$>((:[- thor's mother -])<$>))(=<<)<$>(*)<$>(*2))$1
当我在ghci
中运行这段代码时(在导入Data.Function
和Control.Applicative
之后),ghci
会打印出 2 的所有幂的列表。
这段代码是如何工作的?
【问题讨论】:
我想知道答案是否会是一些傲慢的冒犯......如果是真的,考虑到你为避免粗俗所做的努力,具有讽刺意味。 你尝试过什么? 显而易见的尝试是 (a) 删除注释,(b) 重新格式化/重新缩进代码,(c) 找出哪些实例Functor/Applicative/Monad 正在被使用(可能所有列表,但不要假设......没有什么能阻止一个足够疯狂的程序员在一行代码中使用五个不同的 Monad 实例),(d)尽可能简化你可以。然后看看你还剩下什么。 到目前为止,Haskell 是我最喜欢的编程语言,但 uncyclopedia.wikia.com/wiki/Haskell 让我笑了! 问题由What Compsci textbooks don't tell you: Real world code sucks链接。 当有人发现他们可以在语言 XYZ 中找到的最无端神秘的代码片段时,我真的很生气,然后断言“几乎不可能用语言 XYZ 编写可读代码”。但这只是我…… 【参考方案1】:首先,我们有一个可爱的定义
x = 1 : map (2*) x
如果您以前从未见过它,它本身就有点令人费解。无论如何,这是一个相当标准的懒惰和递归技巧。现在,我们将摆脱使用fix
的显式递归,并使用point-free-ify。
x = fix (\vs -> 1 : map (2*) vs)
x = fix ((1:) . map (2*))
接下来我们要做的是扩展:
部分并使map
变得不必要地复杂。
x = fix ((:) 1 . (map . (*) . (*2)) 1)
好吧,现在我们有两个该常量1
的副本。那永远不会,所以我们将使用阅读器应用程序去重复它。另外,函数组合有点垃圾,所以我们尽可能将其替换为(<$>)
。
x = fix (liftA2 (.) (:) (map . (*) . (*2)) 1)
x = fix (((.) <$> (:) <*> (map . (*) . (*2))) 1)
x = fix (((<$>) <$> (:) <*> (map <$> (*) <$> (*2))) 1)
接下来:对map
的调用太可读了。但是没有什么好害怕的:我们可以使用单子定律来扩展它。特别是fmap f x = x >>= return . f
,所以
map f x = x >>= return . f
map f x = ((:[]) <$> f) =<< x
我们可以point-free-ify,将(.)
替换为(<$>)
,然后添加一些虚假部分:
map = (=<<) . ((:[]) <$>)
map = (=<<) <$> ((:[]) <$>)
map = (<$> ((:[]) <$>)) (=<<)
在上一步中代入这个方程:
x = fix (((<$>) <$> (:) <*> ((<$> ((:[]) <$>)) (=<<) <$> (*) <$> (*2))) 1)
最后,你打破你的空格键并产生美妙的最终方程
x=fix(((<$>)<$>(:)<*>((<$>((:[])<$>))(=<<)<$>(*)<$>(*2)))1)
【讨论】:
你漏掉了- thor's mother -
!【参考方案2】:
正在写一个很长的答案,其中包含导致最终代码的实验的 IRC 日志的完整运行(这是在 2008 年初),但我不小心把所有的文字都写了 :) 虽然没有那么大的损失- 在大多数情况下,丹尼尔的分析是正确的。
这是我开始的:
Jan 25 23:47:23 <olsner> @pl let q = 2 : map (2*) q in q
Jan 25 23:47:23 <lambdabot> fix ((2 :) . map (2 *))
差异主要归结为重构发生的顺序。
我从2 : map ...
开始,而不是 x = 1 : map (2*) x
,并一直保留最初的 2 直到最后一个版本,在那里我挤入了 (*2)
,并将最后的 $2
更改为 $1
. “使地图变得不必要的复杂”步骤并没有发生(那么早)。
我使用 liftM2 而不是 liftA2
在将 liftM2 替换为 Applicative 组合器之前放入了混淆的 map
函数。这也是所有空间消失的时候。
即使是我的“最终”版本也有很多 .
用于函数组合。用<$>
替换所有这些显然发生在从那个和非百科全书之间的几个月里。
顺便说一句,这是一个更新版本,不再提及数字2
:
fix$(<$>)<$>(:)<*>((<$>((:[- Jörð -])<$>))(=<<)<$>(*)<$>(>>=)(+)($))$1
【讨论】:
第一段中“删除”二字的省略是故意的吗?如果是这样,我向你致敬,先生。 @JakeBrownson 这是common internet idiom,虽然我也不确定是不是故意的。【参考方案3】:两个答案都从突然给出的简短原始代码中派生出混淆代码 sn-p,但问题实际上是在询问冗长的混淆代码如何完成其工作。
方法如下:
fix$(<$>)<$>(:)<*>((<$>((:[- thor's mother -])<$>))(=<<)<$>(*)<$>(*2))$1
= - add spaces, remove comment -
fix $ (<$>) <$> (:) <*> ( (<$> ((:[]) <$>) ) (=<<) <$> (*) <$> (*2) ) $ 1
-- \__\______________/_____________________________/
= - A <$> B <*> C $ 1 = A (B 1) (C 1) -
fix $ (<$>) (1 :) ( ( (<$> ((:[]) <$>) ) (=<<) <$> (*) <$> (*2) ) 1 )
-- \__\______________/____________________________/
= - (<$>) A B = (A <$> B) ; (<$> B) A = (A <$> B) -
fix $ (1 :) <$> ( (((=<<) <$> ((:[]) <$>) ) <$> (*) <$> (*2) ) 1 )
-- \\____________________/____________________________/
= - <$> is left associative anyway -
fix $ (1 :) <$> ( ( (=<<) <$> ((:[]) <$>) <$> (*) <$> (*2) ) 1 )
-- \__________________________________________________/
= - A <$> foo = A . foo when foo is a function -
fix $ (1 :) <$> ( ( (=<<) <$> ((:[]) <$>) . (*) . (*2) ) 1 )
-- \__________________________________________________/
= - ((:[]) <$>) = (<$>) (:[]) = fmap (:[]) is a function -
fix $ (1 :) <$> ( ( (=<<) . ((:[]) <$>) . (*) . (*2) ) 1 )
-- \__________________________________________________/
= - ( A . B . C . D) 1 = A (B (C (D 1))) -
fix $ (1 :) <$> (=<<) ( ((:[]) <$>) ( (*) ( (*2) 1 )))
= - (*2) 1 = (1*2) = 2 -
fix $ (1 :) <$> (=<<) ( ((:[]) <$>) ( (*) 2 ))
= - (*) 2 = (2*) -
fix $ (1 :) <$> (=<<) ( ((:[]) <$>) (2*) )
= - ( A <$>) B = A <$> B -
fix $ (1 :) <$> (=<<) ( (:[]) <$> (2*) )
= - A <$> foo = A . foo when foo is a function -
fix $ (1 :) <$> (=<<) ( (:[]) . (2*) )
= - (f . g) = (\ x -> f (g x)) -
fix $ (1 :) <$> (=<<) (\ x -> [2*x] )
= - (=<<) A = ( A =<<) -
fix $ (1 :) <$> ( (\ x -> [2*x] ) =<<)
这里( (\ x -> [2*x]) =<<) = (>>= (\ x -> [2*x])) = concatMap (\ x -> [2*x]) = map (2*)
是一个函数,所以还是<$> = .
:
=
fix $ (1 :) . map (2*)
= - substitute the definition of fix -
let xs = (1 :) . map (2*) $ xs in xs
=
let xs = 1 : [ 2*x | x <- xs] in xs
= - xs = 1 : ys -
let ys = [ 2*x | x <- 1:ys] in 1:ys
= - ys = 2 : zs -
let zs = [ 2*x | x <- 2:zs] in 1:2:zs
= - zs = 4 : ws -
let ws = [ 2*x | x <- 4:ws] in 1:2:4:ws
=
iterate (2*) 1
=
[2^n | n <- [0..]]
是2的所有幂,按递增顺序排列。
这个用途
A <$> B <*> C $ x = liftA2 A B C x
并且由于liftA2 A B C
应用于x
,它是一个函数,作为一个函数意味着liftA2 A B C x = A (B x) (C x)
。
(f `op` g) = op f g = (f `op`) g = (`op` g) f
是three laws of operator sections
>>=
是一元绑定,因为(`op` g) f = op f g
和类型是
(>>=) :: Monad m => m a -> (a -> m b ) -> m b
(\ x -> [2*x]) :: Num t => t -> [ t]
(>>= (\ x -> [2*x])) :: Num t => [ t] -> [ t]
通过类型应用和替换,我们看到有问题的 monad 是 []
,其中 (>>= g) = concatMap g
。
concatMap (\ x -> [2*x]) xs
被简化为
concat $ map (\ x -> [2*x])
=
concat $ [ [2*x] | x <- xs]
=
[ 2*x | x <- xs]
=
map (\ x -> 2*x )
根据定义,
(f . g) x = f (g x)
fix f = let x = f x in x
iterate f x = x : iterate f (f x)
= x : let y = f x in
y : iterate f (f y)
= x : let y = f x in
y : let z = f y in
z : iterate f (f z)
= ...
= [ (f^n) x | n <- [0..]]
在哪里
f^n = f . f . ... . f
-- \_____n_times _______/
这样
((2*)^n) 1 = ((2*) . (2*) . ... . (2*)) 1
= 2* ( 2* ( ... ( 2* 1 )...))
= 2^n , for n in [0..]
【讨论】:
以上是关于这段混淆的 Haskell 代码是如何工作的?的主要内容,如果未能解决你的问题,请参考以下文章