在 Haskell 中展平元组(无点)



我最近拿起了Haskell,我正在尝试编写一个简单的程序来对首字母缩略词进行排序。基本上,我有一个首字母缩略词列表 - 每个首字母缩略词都是一对字符串,例如("DH","Diffie-Hellman") - 我想对它们进行排序并以以下格式打印它们(首字母缩略词,含义,索引)。

例如,如果

acronyms = [("ECDLP", "elliptic curve discrete logarithm problem"),
("DH", "Diffie-Hellman"),
("KDF", "key derivation function")]

那么输出应该是

("DH","Diffie-Hellman",1)
("ECDLP","Elliptic Curve Discrete Logarithm Problem",2)
("KDF","Key Derivation Function",3)

目前,我拥有的代码如下。

main :: IO ()
main =
mapM_
print
(zipWith
(curry (((a, b), c) -> (a, b, c)))
(sort . nub $ map (capitalise <$>) acronyms)
[1 ..])

这里大写是我写的一个函数,它将字符串中每个单词的第一个字符大写。总的来说,这个程序是有效的,但我想得到一些关于如何改进它的反馈。最后,有什么方法可以让这个curry (((a, b), c) -> (a, b, c))成为无意义风格的一部分吗?

谢谢

代码审查

首先,curry实际上只是增加了复杂性。您可以编写一个在没有它的情况下接受多个参数的 lambda。而不是

curry (((a, b), c) -> (a, b, c))

考虑

(a, b) c -> (a, b, c)

现在,你似乎有一个想法,一切都应该是毫无意义的。对于娱乐编程来说,这是一个简洁的练习,在代码高尔夫中很有用(目标是尽可能缩短代码),但是当你编写通用软件时,它通常是一个糟糕的设计选择(目标是使代码尽可能可读)。所以我建议引入一些局部变量和一些额外的函数来处理中介。

我们可以将压缩部分拆分为一个单独的函数

enumerateAcronyms :: [(a, b)] -> [(a, b, Int)]
enumerateAcronyms xs = zipWith ((a, b) c -> (a, b, c)) xs [1..]

请注意,虽然我们使用这个函数作为[(String, String)] -> [(String, String, Int)],但我把它写成[(a, b)] -> [(a, b, Int)]。这种更通用的类型为我们如何处理输入提供了非常有力的保证。也就是说,我们不会修改元组的前两个元素;我们要做的就是移动它们并将整数与它们相关联。

(,) aFunctor实例很奇怪。我花了一分钟才意识到(capitalize <$>)的意思是"对元组的第二个元素做"。所以你应该明确地这样做

(x, y) -> (x, capitalize y)

或者使用更符合您意图的名称。在这种情况下,我们实际上有两种选择:Control.Arrow.secondData.Bifunctor.second.它们做同样的事情,只是基于不同的抽象(前者抽象在函数类型上,后者抽象在数据结构上)。

second capitalize

我个人的偏好是永远不要使用特定于列表的函数,如map,并始终使用通用函子fmap。这样,如果你想把这个函数扩展为通用的,并且处理序列或其他一些数据结构,你就有更少的工作要做。

nub是 O(n^2),如果您保留列表的顺序,这很好。但是,如果您要对列表进行排序,我们可以通过自己滚动来获取 O(n) 格式。所以让我们写我们自己的nub.

同样,我可以用相同的traverse_替换需要MonadmapM_,它只需要Applicative,因此在更一般的情况下工作。

最后,我不太喜欢将长序列的操作链接在一起。有些人这样做,但我不喜欢从右到左阅读我的代码,所以而不是

main :: IO ()
main = traverse_ print (enumerateAcronyms (uniqSort $ map (second capitalise) acronyms))

我可能会写

main :: IO ()
main =
let uniqAcronyms = uniqSort $ map (second capitalise) acronyms
in traverse_ print (enumerateAcronyms uniqAcronyms)

对于总共类似的东西

import Data.Char
import Data.List
import Data.Bifunctor
import Data.Foldable
acronyms :: [(String, String)]
acronyms = [("ECDLP", "elliptic curve discrete logarithm problem"),
("DH", "Diffie-Hellman"),
("KDF", "key derivation function")]
capitaliseFirst :: String -> String
capitaliseFirst [] = []
capitaliseFirst (x:xs) = toUpper x : xs
capitalise :: String -> String
capitalise = unwords . fmap capitaliseFirst . words
enumerateAcronyms :: [(a, b)] -> [(a, b, Int)]
enumerateAcronyms xs = zipWith ((a, b) c -> (a, b, c)) xs [1..]
uniqSort :: Ord a => [a] -> [a]
uniqSort = go . sort
where go [] = []
go [x] = [x]
go (x:x':xs)
| x == x' = go (x':xs)
| otherwise = x : go (x':xs)
main :: IO ()
main =
let uniqAcronyms = uniqSort $ map (second capitalise) acronyms
in traverse_ print (enumerateAcronyms uniqAcronyms)

这比你写的要长得多,但对于普通的Haskell程序员来说,它也会更清楚。在阅读任何特定函数时,需要记住的东西要少得多,因此代码更容易分解。如果你来自Java或其他一些语言,你已经看到了相反的一面:代码太冗长了,并且被分成十个文件,这使得很难看到发生了什么。但是在Haskell中,我们遇到了相反的问题:代码可能会变得太短和时髦,并且很难推理。中间有一个快乐的媒介。

无点

现在,回答你最初的问题:我们可以毫无意义地curry (((a, b), c) -> (a, b, c))吗?我不建议在生产代码中执行此操作,因为它绝对不可读,但让我们探索一下。首先,如前所述,curry不是必需的。这只是

(a, b) c -> (a, b, c)

这是

(a, b) c -> (,,) a b c

那么它只是移动变量并摆脱元组的问题。c可以消除,因此

(a, b) -> (,,) a b

现在我们在左边有一个 2 元组,在右边有两个参数。它们之间的区别是uncurry,因此

(a, b) -> uncurry (,,) (a, b)

然后我们消除最后一个元素

uncurry (,,)

但是,当然,我不知道一目了然,而(a, b) c -> (a, b, c)是不言自明的。

一般来说,我们可以无点任何Haskell函数,只要我们对使用的任何数据结构都有同胚(对于Maybemaybe,对于列表foldr),以及前奏中的一些基本的生活质量函数。你可以使用其他技巧,如阅读器monad或liftA2来使事情变得更容易或更短,但从根本上说,你不需要这样的东西。这只是能够在各种Haskell语法元素周围移动变量的问题。您将递归转换为fix,将模式匹配转换为同胚,并将"错误"顺序的参数转换为多个flip调用。

Lambdabot过去能够使用这种系统方法自动消除大多数Haskell表达式。我不确定它是否仍然存在。

并行列表推导完全消除了元组(zipWithcurry、lambda 和(<$>))的麻烦,我认为这使它更具可读性。此外,由于您无论如何都要排序,因此您不需要nub- 只需group相邻的相等元素就足够了。所以:

{-# Language ParallelListComp #-}
main = mapM_ print
[ (acronym, capitalise meaning, i)
| (acronym, meaning):_ <- group (sort acronyms)
| i <- [1..]
]

我会从让它不那么无点开始!那curry一团糟,一事无成。只需编写一个将元组与索引直接组合在一起的 lambda。

main :: IO ()
main = mapM_ print . zipWith output [1 ..] . sort . nub . map (fmap capitalise) $ acronyms
where output idx (acronym, meaning) = (acronym, meaning, idx)

这很好,但我认为元组的 fmap 实例足够令人惊讶,以至于在我希望其他人阅读的代码中,我也会将该函数命名为:

main :: IO ()
main = mapM_ print . zipWith output [1 ..] . sort . nub . map capitaliseDefinition $ acronyms
where output idx (acronym, meaning) = (acronym, meaning, idx)
capitaliseDefinition (acronym, meaning) = (acronym, capitalise meaning)

你也可以更现代,使用traverse_而不是mapM_.不过,我很同情mapM_的坚持 - 这个名字更明显地表明了它的作用。

最新更新