我最近学到了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))部件成为无点样式呢?
谢谢
发布于 2022-01-19 02:03:28
代码评审
首先,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)]。这种更一般的类型为我们的输入提供了非常有力的保证。也就是说,我们不会修改元组的前两个元素;我们要做的就是移动它们并将整数与它们关联起来。
Functor实例用于(,) a是很奇怪的。我花了一分钟才意识到,(capitalize <$>)的意思是“对元组的第二个元素执行它”。所以你要么显式地这么做
\(x, y) -> (x, capitalize y)或者用一个更符合你意图的名字。在这种情况下,我们实际上有两个选择:Control.Arrow.second或Data.Bifunctor.second。它们做同样的事情,并且简单地基于不同的抽象(前者抽象函数类型,后者抽象数据结构)。
second capitalize我个人的偏好是,永远不要使用特定于列表的函数,比如map,并且总是使用通用函数式fmap。这样,如果您想将此函数扩展为泛型函数,并对序列或其他数据结构进行处理,那么您所要做的工作就少了。
nub是O(n^2),如果要保持列表的顺序,这是很好的。但是如果你对列表进行排序,我们可以通过自己滚动得到它在O(n)中。因此,让我们编写我们自己的nub。
同样,我可以将需要mapM_的Monad替换为相同的traverse_,后者只需要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或其他一些语言,那么您已经看到了它的另一面:代码过于冗长,被分割成10个文件,这使得您很难看到正在发生的事情。但是在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现在我们在左边有两个元组,右边有两个参数。两者之间的区别是uncurry,因此
\(a, b) -> uncurry (,,) (a, b)然后我们消除最后一个元素
uncurry (,,)但当然,我不知道这是什么一目了然,而\(a, b) c -> (a, b, c)是相当不言自明的。
一般来说,只要我们对所使用的任何数据结构都有变质作用 (这是maybe表示Maybe,或者foldr表示列表),以及序曲中的一些基本的生活质量函数,我们就可以不使用任何Haskell函数。您可以使用其他技巧,如读取器monad或liftA2,使事情变得更简单或更短,但从根本上说,您不需要这样的东西。这只是一个能够围绕各种Haskell语法元素移动变量的问题。您可以将递归转换为fix,模式匹配转换为变形,并将处于“错误”顺序的参数转换为几个flip调用。
兰达博特过去能够使用这种系统方法自动释放大多数Haskell表达式。我不确定它是否还在附近。
发布于 2022-01-19 05:31:16
并行列表理解完全消除了元组(zipWith、curry、lambda和(<$>))的模糊,我认为这使它更加可读性更强。另外,由于您正在排序,所以您不需要nub --只需对相邻的等号元素进行group就足够了。所以:
{-# Language ParallelListComp #-}
main = mapM_ print
[ (acronym, capitalise meaning, i)
| (acronym, meaning):_ <- group (sort acronyms)
| i <- [1..]
]发布于 2022-01-19 01:35:51
我会先把它弄得更少--没有任何意义!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_上的人--这个名字更明显地表明了它的作用。
https://stackoverflow.com/questions/70764269
复制相似问题