哈斯克尔高尔夫技巧


73

您在Haskell打高尔夫球有哪些一般秘诀?我正在寻找可以应用于编码高尔夫问题的想法,这些想法至少在某种程度上特定于Haskell。每个答案请只发表一个提示。


如果您是Haskell的高尔夫新手,请参阅Haskell的高尔夫规则指南。还有一个专门的Haskell聊天室:Monads and Men


1
看到到目前为止的答案数量,我对Haskell是否甚至是打高尔夫球的好语言感到怀疑?
Animesh'THE CODER'2014年

10
为什么每个答案只有一个提示?另外,每种语言都是打高尔夫球的好语言。只是不要总是期望胜利。
2014年

6
@unclemeat这样一来,人们可以将优秀的人提升到最高,而又不会提高坏人的满意度,这仅仅是因为他们是由同一个人在相同的答案中编写的。
2014年

3
特殊要求,字符串压缩。
J Atkin

这可能不适合作答,但我仍然想在这里添加:wiki.haskell.org/Prime_numbers_miscellaneous#One-liners
瑕疵的2016年

Answers:


45

定义中缀运算符,而不是二进制函数

每个定义或调用通常节省一两个空间。

0!(y:_)=y
x!(y:z)=(x-1)!z

f 0(y:_)=y
f x(y:z)=f(x-1)z

1字节的运营商可用的符号!#%&,和?。所有其他ASCII标点符号已经被Prelude定义为运算符(例如$),或者在Haskell语法中具有特殊含义(例如@)。

如果需要五个以上的运算符,则可以使用上述的组合,例如!#或某些Unicode标点字符,例如这些(UTF-8中的所有2个字节):

¡ ¢ £ ¤ ¥ ¦ § ¨ © ¬ ® ¯ ° ± ´ ¶ · ¸ ¿ × ÷

11
注意:这也可以用于具有三个或更多参数的函数。(x!y)z=x+y*z并且(x#y)z u=x*z+y*u都工作正常。
Zgarb 2015年

3
也可以将其用于函数参数,例如,\f g(!)x y->f g!x y代替\f g j x y->j(f g)(x y)
Esolanging Fruit 2017年

2
如果您不得不使用圆括号,则将一元函数更改为二进制运算符有时会很有好处- g x=…;g(f x)_?x=…;0!f x
Angs

29

在适当的地方使用无意义(或-free)表示法

具有一个或两个参数的函数通常可以自由编写。

因此,查找元素被交换的元组列表的天真地写为:

revlookup :: Eq b => b -> [(a, b)] -> Maybe a
revlookup e l=lookup e(map swap l)

(该类型只是为了帮助您了解其功能。)

对我们而言,这要好得多:

revlookup=(.map swap).lookup

28

使用清单monad

快速回顾:

xs >> ys        =  concat $ replicate (length xs) ys
xs >>= f        =  concatMap f xs
mapM id[a,b,c]  =  cartesian product of lists: a × b × c
mapM f[a,b,c]   =  cartesian product of lists: f a × f b × f c

例子:

  • 重复两次清单

    Prelude> "aa">>[1..5]
    [1,2,3,4,5,1,2,3,4,5]
    
  • 更短 concatMap

    Prelude> reverse=<<["Abc","Defgh","Ijkl"]
    "cbAhgfeDlkjI"
    
  • 简短concat+列表理解

    Prelude> do x<-[1..5];[1..x]
    [1,1,2,1,2,3,1,2,3,4,1,2,3,4,5]
    
  • 笛卡尔积

    Prelude> mapM id["Hh","io",".!"]
    ["Hi.","Hi!","Ho.","Ho!","hi.","hi!","ho.","ho!"]
    
  • 晶格上的坐标列表

    Prelude> mapM(\x->[0..x])[3,2]
    [[0,0],[0,1],[0,2],[1,0],[1,1],[1,2],[2,0],[2,1],[2,2],[3,0],[3,1],[3,2]]
    

1
我发现有用的其他用法[0..b]>>[a]代替replicate a b
小麦巫师

3
@WheatWizard a<$[1..b]甚至更短replicate
林恩

使用=<<强制您导入Control.Monad。如果由于某些其他原因而不需要该参数,则交换参数并使用它>>=似乎更为简洁。
dfeuer

OTOH,如果Data.Traversable仍然需要,可以将笛卡尔乘积示例简化为for["Hh","io",".!"]id
dfeuer

2
(=<<)实际上在Prelude中!我已经用了很多。
林恩

28

使用警卫不是有条件的:

f a=if a>0 then 3 else 7
g a|a>0=3|True=7

使用分号而不是缩进

f a=do
  this
  that
g a=do this;that

对布尔函数使用布尔表达式

f a=if zzz then True else f yyy
g a=zzz||f yyy

(因此让我分开张贴这些消息让我很痛苦)


2
另外,请使用多个警戒,而不要&&在列表理解内使用。
约翰·德沃夏克

好一个扬-你应该做的是为一个答案,我会投赞成票
bazzargh

5
第一个示例可以通过True=> 进一步缩短1>0
John Dvorak

1
在第一个示例中,我假设您的意思是f a=if a>0 then 3 else 7
Cyoce '16

如果其中没有任何参数,Guard甚至可以工作。
Akangka '16

24

interact :: (String → String) → IO ()

人们通常会忘记此功能的存在-它捕获了所有stdin并将其应用于(纯)功能。我经常看到main-code

main=getContents>>=print.foo

main=interact$show.foo

短很多。就在前奏曲中,因此无需导入!


24

使用GHC 7.10

包含此内容的GHC的第一版已于2015年3月27日发布

这是最新版本,Prelude提供了一些对高尔夫有用的新功能:

(<$>)(<*>)运营商

这些有用的运算符来自Data.Applicative于它!<$>只是fmap,这样你就可以更换map f xfmap f x使用f<$>x无处不在,赢回字节。另外,<*>Applicative列表的实例中很有用:

Prelude> (,)<$>[1..2]<*>"abcd"
[(1,'a'),(1,'b'),(1,'c'),(1,'d'),(2,'a'),(2,'b'),(2,'c'),(2,'d')]

(<$)运营商

x<$a等价于fmap (const x) a; 即用替换容器中的每个元素x

这通常是一个很好的替代方法replicate4<$[1..n]比短replicate n 4

可折叠/可折叠提案

以下功能从处理列表提升[a]为常规Foldable类型t a

fold*, null, length, elem, maximum, minimum, sum, product
and, or, any, all, concat, concatMap

这意味着他们现在也可以在上工作Maybe a,就像“最多包含一个元素的列表”一样。例如null Nothing == True,或sum (Just 3) == 3。同样,length返回0 NothingJust值和1的值。不用写x==Just y就可以写elem y x

您还可以将它们应用于元组,就像您\(a, b) -> [b]先被调用一样。它几乎完全没有用,但是or :: (a, Bool) -> Bool比短一个字符snd,并且elem b比短(==b).snd

Monoid函数memptymappend

并非通常可以挽救生命,但是如果可以推断出类型,mempty它比短一字节Nothing


5
+1很高兴听到有关'<$>`并将<*>其编入序曲!即使它不是编码高尔夫球(适用性很长的话),这也应该很有用。
ankh-morpork

有关单位更换的警告:如果您的语言版本比挑战版本新,则您的解决方案将无济于事。如果要更新现有答案或答案,请不要覆盖现有解决方案。写一个附录。
John Dvorak

4
有趣的[1..2]在那里。那仅仅是[1,2]
骄傲的haskeller 2015年

2
在相同的版本中,我们也从中得到<*Applicative,用于的列表是xs <* ys == concatMap (replicate (length ys)) xs。这是从不同xs >> ysxs *> ys这是concat (replicate (length ys)) xs。在这一点上也要pure短一些return
昂斯,2016年

2
现在,您可以使用<>代替mappend它,它已成为GHC 8.4.1的一部分Prelude
ბიმო

22

使用1<2代替True1>2代替False

g x|x<10=10|True=x
f x|x<10=10|1<2=x

3
这并不是Haskell特有的:它几乎适用于每种布尔类型的语言,而不是其他类型的true和faryy值。
彼得·泰勒

谁能解释一下?
2014年

2
这不是一个很好的例子,我只是想打高尔夫球f=max 10
骄傲的haskeller 2014年

@MasterMastic这只是if(true)用其他语言编写的。在前奏中,否则实际上是布尔值True
自豪的haskeller 2014年

2
@PeterTaylor我认为这对于新的haskellians(像我一样)仍然很有价值,因为我首先学会了使用它otherwise
瑕疵的

20

使用列表理解(巧妙的方式)

每个人都知道它们是有用的语法,通常比map+ lambda 短:

Prelude> [[1..x]>>show x|x<-[1..9]]
["1","22","333","4444","55555","666666","7777777","88888888","999999999"]

filter(以及(可选map)同时):

Prelude> [show x|x<-[1..60],mod 60x<1]
["1","2","3","4","5","6","10","12","15","20","30","60"]

但是,有些怪异的用法有时会派上用场。首先,列表理解根本不需要包含任何<-箭头:

Prelude> [1|False]
[]
Prelude> [1|True]
[1]

这意味着if p then[x]else[]您可以编写[x|p]。另外,要计算满足条件的列表中元素的数量,可以直观地写出:

length$filter p x

但这更短:

sum[1|y<-x,p y]

实际上,我以前曾经使用过所有这些,但我不认为将它们放在这里。
2015年

18

了解你的 Prelude

启动GHCi并滚动浏览Prelude文档。每当您遇到一个具有短名称的函数时,它就会在寻找可能有用的某些情况下得到回报。

例如,假设您希望将字符串s = "abc\ndef\nghi"转换为以空格分隔的字符串"abc def ghi"。明显的方法是:

unwords$lines s

但是,如果您滥用以下内容max,则可以做得更好\n < space < printable ASCII

max ' '<$>s

另一个示例是lex :: String -> [(String, String)],它做了一些非常神秘的事情:

Prelude> lex "   some string of Haskell tokens  123  "
[("some"," string of Haskell tokens  123  ")]

尝试fst=<<lex s从字符串中获取第一个标记,跳过空格。下面是一个使用henkma一个聪明的解决方案lex.showRational价值。


16

匹配常数

列表理解可以在常量上进行模式匹配。


[0|0<-l]

这将提取列表中的0 l,即制作一个与in中一样多的0的列表l


[1|[]<-f<$>l] 

这使得许多名单1的,因为有元素lf需要的空列表(使用<$>作为缀map)。适用sum于计数这些元素。

相比:

[1|[]<-f<$>l]
[1|x<-l,f x==[]]

[x|(0,x)<-l]

常量可以用作模式匹配的一部分。这将提取所有第一项为的元组的第二项0


请注意,所有这些都需要实际的常量文字,而不是变量的值。例如,let x=1 in [1|x<-[1,2,3]]将输出[1,1,1],而不是[1],因为外部x绑定已被覆盖。


14

使用words而不是一长串的字符串。这并不是Haskell特有的,其他语言也有类似的技巧。

["foo","bar"]
words"foo bar"  -- 1 byte longer
["foo","bar","baz"]
words"foo bar baz"  -- 1 byte shorter
["foo","bar","baz","qux"]
words"foo bar baz qux"    -- 3 bytes shorter

14

了解您的单子函数

1)
使用来模拟单子函数mapM

很多时候会有代码sequence(map f xs),但是可以用代替mapM f xs。即使仅sequence单独使用也会更长mapM id

2)
使用(>>=)(或(=<<))组合功能

函数monad的版本(>>=)定义如下:

(f >>= g) x = g (f x) x 

这对于创建不能表示为管道的函数很有用。例如,\x->x==nub x长于nub>>=(==),且\t->zip(tail t)t长于tail>>=zip


+1-我什至没有意识到函数有一个monad实例。那可能非常方便:)
Jules

2
旁注:尽管它是其中的一部分,但也Applicative没有Monad实现,pure它比const以前更短并且实际上对我有所帮助。
ბიმო

14

参数可以短于定义

我只是以一种非常奇怪的方式被汉克玛打败了

如果f您答案中的辅助函数使用的运算符在您的答案中的其他地方没有使用,并且f被调用过一次,请将该运算符设为的参数f

这个:

(!)=take
f a=5!a++3!a
reverse.f

比这长两个字节:

f(!)a=5!a++3!a
reverse.f take

12

使用cons运算子(:)

串联列表时,如果第一个列表的长度为1,则:改为使用。

a++" "++b
a++' ':b  -- one character shorter

[3]++l
3:l    -- three characters shorter

4
值得注意的是,它是正确的关联对象,因此您可以在列表的开头继续将其用于任意数量的单个项目,例如1:2:3:x而不是[1,2,3]++x
Jules

11

不要经常使用反引号。反引号是制作部分前缀函数的不错工具,但有时会被误用。

一旦我看到有人写这个子表达式:

(x`v`)

虽然和刚才一样v x

另一个例子是(x+1)`div`y 与相对div(x+1)y

我发现这种情况经常发生divelem因为这些功能通常在常规代码中用作中缀。


您不是要对前缀函数进行划分吗?
Cyoce '16

@Cyoce是的,当然
骄傲的haskeller

11

使用图案防护

它们比let解构您所定义函数的参数的a或lambda 短。这有助于当你需要像fromJust来自Data.Maybe

f x=let Just c=… in c

超过

f x=(\(Just c)->c)$…

超过

m(Just c)=c;f x=m$…

超过

f x|Just c<-…=c

实际上,即使绑定一个普通的旧值而不是进行解构,它们也更短:请参见xnor的技巧


好吧,lambda不需要美元符号,而且这种变化似乎使该片段和下一个片段的长度相同
骄傲的haskeller

1
我假设e实际上不是一个令牌,而是$在它之前需要一个更长的表达式,通常是这种情况。
林恩

11

有条件的短

last$x:[y|b]

相当于

if b then y else x

运作方式如下:

             [y|b]   x:[y|b]   last$x:[y|b]
if...      +--------------------------------
b == False | []      [x]       x
b == True  | [y]     [x,y]     y

应该是if b then y else x吗?
Akangka '16

@ChristianIrwan好,是的。
xnor

不会使用bool更短的表格,因为您不需要列表理解
Potato44 '17

@ Potato44在Data.Bool中,它需要导入字节。
xnor

11

使用减号

减号-是许多语法规则的烦人例外。本技巧列出了在Haskell中表达否定和减法的一些简短方法。如果我错过了什么,请告诉我。

否定

  • 要否定一个表达式e,只需执行-e。例如,-length[1,2]给出-2
  • 如果e是中等程度的复杂,则需要用括号括起来e,但是通常可以通过将其移来节省一个字节:-length(take 3 x)比短-(length$take 3 x)
  • 如果if e的前导=固定性小于6 的infix运算符,则需要一个空格:f= -2定义fk< -2测试if k小于-2。如果固定度为6或更高,则需要parens:2^^(-2)0.25。通常,您可以重新整理内容以摆脱这些问题:例如,使用do -k>2代替k< -2
  • 同样,如果!是操作符,则将-a!b其解析为(-a)!b固定性!最多为6(-1<1给定True),-(a!b)否则为(-[1,2]!!0给定-1)。用户定义的运算符和反引号函数的默认固定性为9,因此它们遵循第二条规则。
  • 要将否定变为函数(与mapetc 一起使用),请使用(0-)

减法

  • 要获得减去功能k,使用一节(-k+),它增加了-kk甚至可以是一个非常复杂的表达式:(-2*length x+)按预期工作。
  • 要减去1,请pred改用,除非在两边都需要一个空格。这是罕见的,通常与发生until或用户定义的功能,因为map pred x可以通过替换pred<$>xiterate pred x通过[x,x-1..]。而且,如果您在f pred x某处,则f无论如何都应该将其定义为infix函数。看到这个技巧

11

尝试重新排列函数定义和/或参数

通过更改函数定义中模式匹配用例的顺序,有时可以节省几个字节。这些节省很便宜,但很容易忽略。

例如,请考虑以下早期版本的(部分)此答案

(g?x)[]=x
(g?x)(a:b)=g(g?x$b)a

这是的递归定义?,基本情况为空列表。由于[]这不是一个有用的值,因此我们应该交换定义,并用通配符_或虚拟参数替换它y,并保存一个字节:

(g?x)(a:b)=g(g?x$b)a
(g?x)y=x

根据相同的答案,考虑以下定义:

f#[]=[]
f#(a:b)=f a:f#b

空列表出现在返回值中,因此我们可以通过交换大小写来节省两个字节:

f#(a:b)=f a:f#b
f#x=x

同样,函数自变量的顺序有时会因允许您删除不必要的空格而有所不同。考虑此答案的早期版本:

h p q a|a>z=0:h p(q+2)(a-1%q)|1<2=1:h(p+2)q(a+1%p)

在第一个分支之间h和之间有一个烦人的空格p。我们可以通过定义h a p q来代替它h p q a

h a p q|a>z=0:h(a-1%q)p(q+2)|1<2=1:h(a+1%p)(p+2)q

10

不要浪费“否则”的警卫

最终保护是万能的True(简称1>0),可以用来绑定变量。相比:

... |1>0=1/(x+y)
... |z<-x+y=1/z

... |1>0=sum l-sum m
... |s<-sum=s l-s m

由于警卫是强制性的,否则会造成浪费,因此几乎不需要这样做。保存一对括号或绑定使用两次的length-3表达式就足够了。有时,您可以否定保护符,以使最后一种情况成为最能使用绑定的表达式。


10

使用,而不是&&守卫

可以将所有必须持有的守卫中的多个条件结合起来,代替&&

f a b | a/=5 && b/=7 = ...
f a b | a/=5 ,  b/=7 = ...

2
也不一定是条件,您可以执行以下操作:f xs m | [x] <- xs, Just y <- m, x > 3 = y
BlackCap

10

本地声明的语法较短

有时候,你需要定义一个本地函数或操作,但它的成本大量的字节写wherelet…in或通过添加额外的参数给它提升至顶级。

g~(a:b)=2!g b where k!l=k:take(a-1)l++(k+1)!drop(a-1)l
g~(a:b)=let k!l=k:take(a-1)l++(k+1)!drop(a-1)l in 2!g b
g~(a:b)=2!g b$a;(k!l)a=k:take(a-1)l++((k+1)!drop(a-1)l)a

幸运的是,Haskell对于本地声明具有一种令人困惑且很少使用但合理的简洁语法:

fun1 pattern1 | let fun2 pattern2 = expr2 = expr1

在这种情况下:

g~(a:b)|let k!l=k:take(a-1)l++(k+1)!drop(a-1)l=2!g b

您可以将此语法与多语句声明或多个声明一起使用,甚至嵌套:

fun1 pattern1 | let fun2 pattern2 = expr2; fun2 pattern2' = expr2' = expr1
fun1 pattern1 | let fun2 pattern2 = expr2; fun3 pattern3 = expr3 = expr1
fun1 pattern1 | let fun2 pattern2 | let fun3 pattern3 = expr3 = expr2 = expr1

它也适用于绑定变量或其他模式,尽管除非您也绑定了函数,否则模式保护通常会更短一些。


3
这也适用于列表推导:[f 1|let f x=x+1]
Laikoni

10

避免 repeat n

-- 8 bytes, whitespace might be needed before and after
repeat n

-- 8 bytes, whitespace might be needed before
cycle[n]

-- 7 bytes, whitespace might be needed before and after, can be reused,
-- needs an assignment, n needs to be global
l=n:l;l

-- 7 bytes, never needs whitespace, n needs to derive from Enum,
-- n has to be short enough to be repeated twice
[n,n..]

这四个表达式中的任何一个都会产生的无限列表n

这是一个非常具体的提示,但最多可以节省3个字节!


4
如果n为global,l=n:l;l则长度相同,并且适用于(某些)较长的表达式。(不过可能需要空格。)
ØrjanJohansen

@ØrjanJohansen我将其合并到帖子中。谢谢!
完全人类

10

当一个结果为空列表时,条件条件更短

当您需要根据某些条件返回列表A或空列表的[]条件时C,可以使用一些更短的替代条件条件构造:

if(C)then(A)else[] -- the normal conditional
last$[]:[A|C]      -- the golfy all-round-conditional
concat[A|C]        -- shorter and works when surrounded by infix operator
id=<<[A|C]         -- even shorter but might conflict with other infix operators
[x|C,x<-A]         -- same length and no-conflict-guarantee™
[0|C]>>A           -- shortest way, but needs surrounding parenthesis more often than not

实施例:12


第二个具有A[]切换。
与Orjan约翰森

@ØrjanJohansen已更正,谢谢!
Laikoni '17

1
啊哈!但*>具有高于固定性>>(还是有点低了舒适性。)
与Orjan约翰森

9

Lambda解析规则

Lambda表达式实际上并不需要括号-它只是贪婪地抓取了所有内容,因此整个过程仍然可以解析,例如直到

  • 结束语- (foo$ \x -> succ x)
  • 一个- let a = \x -> succ x in a 4
  • 行的结尾- main = getContents>>= \x -> head $ words x
  • 等等..

遇到这种情况,在某些奇怪的情况下可以节省一两个字节。我相信\也可以用来定义运算符,因此在利用此运算符时,直接在运算符之后编写一个lambda时将需要一个空间(如第三个示例)。

这是一个示例,其中我可以弄清楚使用lambda是最短的事情。该代码基本上看起来像:

a%f=...
f t=sortBy(% \c->...)['A'..'Z']

9

替换let为lambda

通常,这可以缩短一个单独的辅助定义,该辅助定义由于某种原因而无法与防护绑定或全局定义。例如,替换

let c=foo a in bar

短3个字节

(\c->bar)$foo a

对于多个辅助定义,取决于定义的数量,增益可能较小。

let{c=foo a;n=bar a}in baz
(\c n->baz)(foo a)$bar a

let{c=foo a;n=bar a;m=baz a}in qux
(\c n m->qux)(foo a)(bar a)$baz a

let{c=foo a;n=bar a;m=baz a;l=qux a}in quux
(\c n m l->quux)(foo a)(bar a)(baz a)$qux a

如果某些定义引用了其他定义,则以这种方式保存字节甚至更加困难:

let{c=foo a;n=bar c}in baz
(\c->(\n->baz)$bar c)$foo a

主要警告是,let允许您定义多态变量,但@@ ChristianSievers指出,lambda不能。例如,

let f=length in(f["True"],f[True])

结果(1,1),但

(\f->(f["True"],f[True]))length

给出类型错误。


1
这几乎无关紧要,但是“在语义上等效”的承诺太大了。我们有多态性let,所以我们可以做到let f=id in (f 0,f True)。如果我们尝试使用lambda重写它,则不会键入check。
Christian Sievers

@ChristianSievers是的,谢谢你的来信。我对其进行了编辑
。– Zgarb

8

使用守卫绑定

定义命名函数时,可以将表达式绑定到防护中的变量。例如,

f s|w<-words s=...

与...相同

f s=let w=words s in ...
f s=(\w->...)$words s

使用它来保存重复的表达式。两次使用该表达式时,即使长度和优先级问题都可以更改该长度,它在长度6处的收支平衡。

(在示例中,如果s未使用原始变量,则这样做的时间要短一些

g w=...
f=g.words

但这不适用于绑定更复杂的表达式。)


这不是该答案的重复/特殊情况吗?
林恩

@Lynn回想起来,这是一个特例,但是当我读到这个答案时,该Just示例使我认为是从容器中提取而不是存储在表达式中的模式匹配。
xnor

8

用于(0<$)代替length比较

测试列表a是否长于列表时b,通常会写

length a>length b

但是,用相同的值(例如)替换两个列表的每个元素0,然后比较这两个列表会更短:

(0<$a)>(0<$b)

在线尝试!

括号是因为需要<$和比较操作符(==><=,...)具有相同的优先级4,但在其他一些情况下可能并不需要它们,节省更多的字节。


8

更短 transpose

要使用该transpose功能Data.List,必须先导入。如果这是唯一需要导入的函数,则可以使用以下foldr定义保存字节transpose

import Data.List;transpose
e=[]:e;foldr(zipWith(:))e

请注意,该行为仅对于具有相同长度的列表列表是相同的。

在这里成功使用了它。


8

获取后缀

使用scanr(:)[]以获取列表的后缀:

λ scanr(:)[] "abc"
["abc","bc","c",""]

这比tails之后要短得多import Data.List。您可以使用scanr(\_->init)=<<id(由ØrjanJohansen找到)做前缀。

λ  scanr(\_->init)=<<id $ "abc"
["","a","ab","abc"]

这样可以节省一个字节

scanl(\s c->s++[c])[]

也许scanl(flip(:))[] "abc"= ["","a","ba","cba"]也值得一提–有时前缀向后无关紧要。
林恩

3
ØrjanJohansen发现前缀的替代品短了一个字节:scanr(\_->init)=<<id
Laikoni
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.