作为一名从业者,我为什么要关心Haskell?什么是单子电池,为什么我需要它?[关闭]


9

我只是不知道他们解决了什么问题。



2
我认为此编辑有些极端。我认为您的问题本质上是一个好问题。只是其中的某些部分有点……有争议。这可能只是尝试学习您未了解的内容而感到沮丧的结果。
詹森·贝克

@SnOrfus,我是那个问题的混蛋。我懒得去编辑它。
工作

Answers:


34

单子弹既不是好事也不是坏事。他们就是。它们是用于解决问题的工具,就像许多其他编程语言构造一样。它们的一个非常重要的应用是使使用纯函数式语言工作的程序员的工作变得更轻松。但是它们在非功能语言中很有用;只是人们很少意识到他们正在使用Monad。

什么是单子?想到Monad的最佳方法是将其作为设计模式。在I / O的情况下,您可能会认为它只是美化的管道,而全局状态是各个阶段之间传递的东西。

例如,让我们以您正在编写的代码为例:

do
  putStrLn "What is your name?"
  name <- getLine
  putStrLn ("Nice to meet you, " ++ name ++ "!")

这里发生的事情比目光所及还要多。例如,您会注意到它putStrLn具有以下签名: putStrLn :: String -> IO ()。为什么是这样?

这样考虑:让我们假装(为简单起见)stdout和stdin是我们唯一可以读写的文件。用命令式语言,这没问题。但是用功能语言,您不能改变全局状态。函数只是一个需要一个(或多个)值并返回一个(或多个)值的东西。解决此问题的一种方法是使用全局状态作为传入和传出每个函数的值。因此,您可以将第一行代码转换为类似以下内容的代码:

global_state <- (\(stdin, stdout) -> (stdin, stdout ++ "What is your name?")) global_state

...并且编译器会知道要打印添加到的第二个元素中的任何内容global_state。现在我不了解您,但是我不喜欢那样编程。简化此操作的方法是使用Monads。在Monad中,您传递一个值,该值表示从一个动作到下一个动作的某种状态。这就是为什么putStrLn返回类型为的原因IO ():它正在返回新的全局状态。

那你为什么在乎呢? 好吧,函数式编程相对于命令式编程的优势已经在多个地方争论不休,所以我一般不会回答这个问题(但如果您想了解函数式编程的情况,请参阅本文)。但是,对于这种特定情况,如果您了解Haskell想要完成的工作可能会有所帮助。

许多程序员认为Haskell试图阻止他们编写命令性代码或使用副作用。那不是真的。这样考虑:命令式语言默认情况下允许副作用,但是如果您确实愿意(并且愿意处理某些需要的扭曲),则允许您编写功能代码。默认情况下,Haskell纯粹是功能性的,但是如果您确实愿意的话,它允许您编写命令性代码(如果程序有用,则可以这样做)。重点并不是要使编写具有副作用的代码变得困难。这是为了确保您明确具有副作用(使用类型系统来强制这样做)。


6
最后一段是黄金。对其进行一些提取和解释:“命令式语言默认情况下允许副作用,但如果您确实愿意,则允许您编写功能代码。功能语言默认情况下纯粹是功能性的,但允许您编写命令性代码如果你真的想的话。”
Frank Shearar 2011年

值得注意的是,您链接到的论文一开始就明确拒绝了“不变性作为函数式编程的优点”。
梅森·惠勒

@MasonWheeler:我读了那些段落,不是在否认不变性的重要性,而是将其拒绝作为证明函数式编程优越性的有说服力的论据。实际上,他在goto稍后的文章中谈到了消除(作为结构化编程的一种论据)的同一件事,将这种论据描述为“无结果的”。但是我们当中没有人暗中希望goto返回。只是,您不能认为对于goto广泛使用它的人来说没有必要。
罗伯特·哈维

7

我会咬!Monads本身并不是Haskell的存在理由(早期版本的Haskell甚至都没有)。

您的问题有点像说“ C ++,当我看语法时,我感到很无聊。但是模板是C ++的一个广为宣传的功能,所以我看了另一种语言的实现”。

Haskell程序员的发展只是个玩笑,并不是要认真对待。

在Haskell中,用于程序的Monad是Monad类型类的实例,也就是说,它恰好支持某些少量操作。Haskell对实现Monad类型类的类型提供特殊支持,特别是语法支持。实际上,这就是所谓的“可编程分号”。当您将此功能与Haskell的其他一些功能(一流的功能,默认情况下是惰性)结合使用时,您将获得的功能是将某些东西实现为传统上被认为是语言功能的库。例如,您可以实现异常机制。您可以将对延续和协程的支持实现为库。Haskell,该语言不支持可变变量:

您询问“也许/身份/安全部门单子?”。Maybe monad是一个示例,说明了如何将异常处理(非常简单,只有一个异常)实现为库。

没错,编写消息和阅读用户输入不是很独特。IO是“ monad作为功能”的糟糕示例。

但是要进行迭代,将其本身的“功能”(例如Monads)与语言的其余部分隔离开并不一定会立即显得有用(C ++ 0x的一项重大新功能是右值引用,并不意味着您可以采用它们脱离了C ++的上下文,因为它的语法使您感到厌烦,并且一定会看到实用程序)。不能通过在存储桶中添加大量功能来获得编程语言。


实际上,haskell确实通过ST monad(按其自身规则运行的语言中为数不多的不可思议的神奇魔法部分之一)支持可变变量。
萨拉

4

程序员都编写程序,但是相似之处到此结束。我认为程序员的差异远远超出了大多数程序员的想象。采取任何长期的“战斗”,例如静态变量类型与运行时类型,脚本与编译,C样式与面向对象。您将发现不可能合理地辩称某个阵营不如人,因为其中有些人在某些编程系统中推出了对我而言毫无意义甚至彻头彻尾无法使用的优秀代码。

我认为不同的人会以不同的方式思考,如果您不被语法糖或特别是仅为了方便而存在的抽象所吸引,并且实际上却花费了可观的运行时成本,那么就一定不要使用此类语言。

但是,我建议您至少尝试使自己熟悉所放弃的概念。只要他们真正了解lambda表达式有什么大不了,我就不会反对那些热衷纯C的人。我怀疑大多数人不会立即成为粉丝,但是至少当他们找到完美的问题时,它会出现在他们的脑海中,用lambdas可以解决的问题那么容易。

而且,最重要的是,要避免被狂热的说话惹恼,尤其是那些实际上不知道他们在说什么的人。


4

Haskell强制执行参照透明性:给定相同的参数,无论您调用该函数多少次,每个函数始终返回相同的结果。

例如,这意味着在Haskell(没有Monads)上,您不能实现随机数生成器。在C ++或Java中,您可以使用全局变量来完成此操作,并存储随机数发生器的中间“种子”值。

在Haskell上,全局变量的对应对象是Monads。


那么...如果您想要一个随机数生成器怎么办?它不是功能吗?即使没有,我如何获得一个随机数发生器?
Job

@Job您可以在monad内部创建一个随机数生成器(基本上是一个状态跟踪器),也可以使用unsafePerformIO,这是绝对不应该使用的Haskell魔鬼(实际上,如果使用随机性,可能会破坏程序)里面!)
替代

在Haskell中,您可以绕过“ RandGen”,这基本上是RNG的当前状态。因此,生成新随机数的函数采用RandGen,然后返回包含新RandGen和产生的数字的元组。另一种选择是在某个地方指定一个介于最小值和最大值之间的随机数列表。这将返回一个惰性评估的无限数字流,因此只要需要新的随机数,我们就可以遍历此列表。
qqwy

用任何其他语言获取它们的方式都一样!您掌握了一些伪随机数生成器算法,然后为它添加了一些值,然后弹出“随机”数!唯一的区别是,诸如C#和Java之类的语言会使用系统时钟或类似的东西自动为您植入PRNG。这样的事实和事实是,在haskell中,您还可以使用新的PRNG来获取“下一个”数字,而在C#/ Java中,这全部是使用Random对象中的可变变量在内部完成的。
萨拉

4

有点老问题了,但这是一个非常好的问题,所以我来回答。

您可以将monad视为代码块,可以完全控制它们的执行方式:每行代码应返回什么,执行是否应在任何时候停止,每行之间是否应进行其他处理。

我将举例说明单子使之实现的功能,否则将很难实现。这些示例都不在Haskell中,只是因为我对Haskell的了解有点动摇,但是它们都是Haskell如何激发单子的启发的示例。

解析器

通常,如果您想编写某种解析器(例如,要实现一种编程语言),则必须阅读BNF规范并编写一堆循环代码以对其进行解析,或者必须使用编译器编译器例如Flex,Bison,yacc等。但是使用monad,您可以在Haskell中创建一种“编译器解析器”。

没有monad或yacc,bison等专用语言就无法真正完成解析器。

例如,我采用了针对IRC协议的BNF语言规范:

message    =  [ ":" prefix SPACE ] command [ params ] crlf
prefix     =  servername / ( nickname [ [ "!" user ] "@" host ] )
command    =  1*letter / 3digit
params     =  *14( SPACE middle ) [ SPACE ":" trailing ]
           =/ 14( SPACE middle ) [ SPACE [ ":" ] trailing ]

nospcrlfcl =  %x01-09 / %x0B-0C / %x0E-1F / %x21-39 / %x3B-FF
                ; any octet except NUL, CR, LF, " " and ":"
middle     =  nospcrlfcl *( ":" / nospcrlfcl )
trailing   =  *( ":" / " " / nospcrlfcl )

SPACE      =  %x20        ; space character
crlf       =  %x0D %x0A   ; "carriage return" "linefeed"

并将其压缩为F#(这是另一种支持monad的语言)中的约40行代码:

type UserIdentifier = { Name : string; User: string; Host: string }

type Message = { Prefix : UserIdentifier option; Command : string; Params : string list }

let space = character (char 0x20)

let parameters =
    let middle = parser {
        let! c = sat <| fun c -> c <> ':' && c <> (char 0x20)
        let! cs = many <| sat ((<>)(char 0x20))
        return (c::cs)
    }
    let trailing = many item
    let parameter = prefixed space ((prefixed (character ':') trailing) +++ middle)
    many parameter

let command = atLeastOne letter +++ (count 3 digit)

let prefix = parser {
    let! name = many <| sat (fun c -> c <> '!' && c <> '@' && c <> (char 0x20))   //this is more lenient than RFC2812 2.3.1
    let! uh = parser {
        let! user = maybe <| prefixed (character '!') (many <| sat (fun c -> c <> '@' && c <> (char 0x20)))
        let! host = maybe <| prefixed (character '@') (many <| sat ((<>) ' '))
        return (user, host)
    }
    let nullstr = function | Some([]) -> null | Some(s) -> charsString s | _ -> null
    return { Name = charsString name; User = nullstr (fst uh); Host = nullstr (snd uh) }
}

let message = parser {
    let! p = maybe (parser {
        let! _ = character ':'
        let! p = prefix
        let! _ = space
        return p
    })
    let! c = command
    let! ps = parameters
    return { Prefix = p; Command = charsString c; Params = List.map charsString ps }
}

与Haskell的语法相比,F#的monad语法非常丑陋,我可能会对此进行一些改进-但要领会的是,从结构上讲,解析器代码与BNF相同。如果没有monad(或解析器生成器),这不仅会花费更多的工作,而且与规范几乎没有相似之处,因此阅读和维护都非常糟糕。

自定义多任务

通常,多任务被认为是OS的功能-但对于monad,您可以编写自己的调度程序,以便在每条指令monad之后,程序将控制权交给调度程序,然后调度程序将选择另一个monad执行。

一个人制作了一个“任务”单子来控制游戏循环(同样在F#中),因此不必将所有内容编写为对每次Update()调用起作用的状态机,他只需编写所有指令就好像它们是一个函数一样。 。

换句话说,不必执行以下操作:

class Robot
{
   enum State { Walking, Shooting, Stopped }

   State state = State.Stopped;

   public void Update()
   {
      switch(state)
      {
         case State.Stopped:
            Walk();
            state = State.Walking;
            break;
         case State.Walking:
            if (enemyInSight)
            {
               Shoot();
               state = State.Shooting;
            }
            break;
      }
   }
}

您可以执行以下操作:

let robotActions = task {
   while (not enemyInSight) do
      Walk()
   while (enemyInSight) do
      Shoot()
}

LINQ转SQL

LINQ to SQL实际上是monad的示例,并且类似的功能可以在Haskell中轻松实现。

因为我不太准确地记住所有内容,所以我不会详细介绍,但是Erik Meijer很好地解释了这一点


1

如果您熟悉GoF模式,则单子单元就像将Decorator模式和Builder模式放到类固醇上,并被放射性r咬住。

上面有更好的答案,但是我看到的一些特定好处是:

  • monads在不更改核心类型的情况下用其他属性装饰了某些核心类型。例如,一个monad可能会“举起” String并添加“ isWellFormed”,“ isProfanity”或“ isPalindrome”等值。

  • 同样,monad允许将简单类型组合为集合类型

  • monad允许将函数后期绑定到此高阶空间中

  • monads允许在高阶空间中将任意函数和参数与任意数据类型混合

  • monad允许将纯无状态的功能与不纯的有状态的基础混合在一起,因此您可以跟踪问题出在哪里

List是Java中常见的monad示例。它需要一些核心类(例如String),并将其“提升”到List的monad空间中,从而添加有关列表的信息。然后它将新功能绑定到该空间中,如get(),getFirst(),add(),empty()等。

大规模地想象,您不用编写程序,而是编写了一个大型的Builder(作为GoF模式),最后的build()方法吐出了程序应该产生的答案。而且您可以在不重新编译原始代码的情况下向ProgramBuilder添加新方法。这就是为什么monad是强大的设计模型。

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.