2014-03-31 61 views
6

我是Haskell的初学者。哈斯克尔函数定义约定

在函数定义中使用按我校材料的约定实际上是如下

函数名arguments_separated_by_spaces = code_to_do

例如:

f a b c = a * b +c 

作为学生学习数学的我习惯于使用功能如下:

function_name(arguments_separated_by_commas)= code_to_do

例如:

f(a,b,c) = a * b + c 

它在Haskell工作。

我的疑问是它是否适用于所有情况?

我的意思是我可以在Haskell函数定义中使用传统的数学约定吗?

如果错了,在哪些特定情况下会议出错?

感谢提前:)

回答

13

假设你想定义一个函数来计算右三角形的hypoteneuse的平方。以下定义中的任何一个都是有效的

hyp1 a b = a * a + b * b 

hyp2(a,b) = a * a + b * b 

但是,它们不是相同的功能!您可以通过在GHCI

看着自己的类型
>> :type hyp1 
hyp1 :: Num a => a -> a -> a 

>> :type hyp2 
hyp2 :: Num a => (a, a) -> a 

hyp2第一(现在忽略Num a =>一部分)的类型告诉你的函数取一对(a, a)并返回另一个a(例如,它可能需要告诉一对整数并返回另一个整数或一对实数并返回另一个实数)。你这样使用它

>> hyp2 (3,4) 
25 

请注意,括号在这里不是可选的!他们确保论证是正确的类型,一对a s。如果你不包含它们,你会得到一个错误(现在看起来可能会让你感到困惑,但请放心,当你了解了类型类时它会有意义的)。

现在看hyp1读取类型a -> a -> a的一种方法是需要两件东西a并返回a类型的其他东西。您可以使用它像这样

>> hyp1 3 4 
25 

现在你会得到一个错误,如果你包括括号!

所以首先要注意的是,你使用函数的方式必须与你定义它的方式相匹配。如果用parens定义函数,则每次调用它时都必须使用parens。如果在定义函数时不使用parens,则在调用它时不能使用它们。

因此,似乎没有理由选择其中一个 - 这只是一个品味问题。但实际上,我认为是一个优先于另一个的好理由,并且您应该更喜欢没有括号的样式。有三个很好的理由:

  1. 它看起来更干净,让您的代码更容易阅读,如果你没有parens凌乱的页面。

  2. 如果你在任何地方都使用parens,那么你会受到性能影响,因为每次使用该函数时都需要构造和解构一对(尽管编译器可能会优化它 - 我不确定)。

  3. 你想要得到的好处讨好,又名部分应用功能 *。

最后一点是微妙的。回想一下,我说过一种理解a -> a -> a类型函数的方法是它需要两件事a类型,并返回另一个a。但是还有另一种方法来读取该类型,即a -> (a -> a)。这意味着完全一样的东西,因为->运算符在Haskell中是右关联的。解释是该函数只需要一个a,并返回a -> a类型的函数。这可以让你只需要提供的第一个参数的功能,并应用第二个参数后,例如

>> let f = hyp1 3 
>> f 4 
25 

这是在各种各样的场合实用价值。例如,map功能可让您将一些功能列表中的每个元素 -

>> :type map 
map :: (a -> b) -> [a] -> [b] 

假设你有增加了一个爆炸任何String功能(++ "!")。但是你有Strings的列表,你希望它们都以爆炸声结束。没问题!你只是部分应用map功能

>> let bang = map (++ "!") 

现在bang是类型的函数**

>> :type bang 
bang :: [String] -> [String] 

,你可以用它像这样

>> bang ["Ready", "Set", "Go"] 
["Ready!", "Set!", "Go!"] 

非常有用的!

我希望我已经说服你,在你的学校的教育材料中使用的惯例有一些相当坚实的理由被使用。作为拥有数学背景的人,我可以看到使用更多“传统”语法的吸引力,但我希望随着您在编程之旅中的进步,您将能够看到改变最初的某些东西的优势对你不熟悉。


*对于书呆子的说明 - 我知道咖喱和部分应用并不完全一样。

**实际上,GHCI会告诉您类型为bang :: [[Char]] -> [[Char]],但由于String[Char]的同义词,所以这些意思是相同的。

+0

在你的第一个笔记上:'不完全相同的东西'暗示着某种关系,而实际上没有关系。 Currying只允许轻松部分应用,但我认为原则上它们是非常不相关的。 – Xeo

+2

@Xeo他们有些相关。 curried/tupled表单形成一个[adjunction](http://en.wikipedia.org/wiki/Adjoint_functors),其中F是'( - x B)',G是' -^B'。 – jozefg

+0

@Xeo我想这是一个味道点。对于一个函数'(a,b,c) - > d',currying它给出'a - > b - > c - > d'而部分应用第一个参数给出a - >(b,c) - > D'。对于两个参数的函数,currying和部分应用是相同的事情。在Haskell中,几乎没有区别,因为默认情况下是currying(这是我希望避免用我的脚注做出的评论!) –

2
f(a,b,c) = a * b + c 

关键的区别理解的是,上述功能需要一个三人间和给出结果。你实际上在做什么是三重模式匹配。该类型的上述功能是这样的:

(a, a, a) -> a 

如果你写的功能是这样的:

f a b c = a * b + c 

你得到的功能自动咖喱。 你可以写这样的东西,如let b = f 3 2,它会检查,但同样的事情不会用你的初始版本。此外,像柯里化这样的东西可以帮助很多,同时使用(.)来编写各种功能,除非您尝试编写三元组,否则前者的风格无法实现。

2
  1. 数学符号不一致。如果所有的功能使用(,)给定的参数,你就必须写(+)((*)(a,b),c)通过a*bc运作+ - 当然,a*b被传递ab运作*制定。

  2. 可以以tupled形式写入所有内容,但定义构图要困难得多。鉴于现在您可以指定一个类型a->b来涵盖任何元组的功能(因此,您可以将组合定义为类型(b->c)->(a->b)->(a->c)的函数),但使用元组来定义任意元组的功能要复杂得多(现在a->b只能表示一个函数的一个参数;你不能再用多个参数的函数组成一个具有许多参数的函数)。所以,技术上可能的,但它需要一个语言功能,使其简单方便。