RError.com

RError.com Logo RError.com Logo

RError.com Navigation

  • 主页

Mobile menu

Close
  • 主页
  • 系统&网络
    • 热门问题
    • 最新问题
    • 标签
  • Ubuntu
    • 热门问题
    • 最新问题
    • 标签
  • 帮助
主页 / 问题 / 908882
Accepted
ZX-SPECTRUM
ZX-SPECTRUM
Asked:2020-11-20 04:44:05 +0000 UTC2020-11-20 04:44:05 +0000 UTC 2020-11-20 04:44:05 +0000 UTC

如何在没有变量的情况下进行编程?关于功能的问题

  • 772

当我从事 Python 和 C 的重复时,我的手不会以任何方式到达 Haskell。这个问题已经很久没有离开我的脑海了)我可以以某种方式简要回答它吗?

scala
  • 3 3 个回答
  • 10 Views

3 个回答

  • Voted
  1. Best Answer
    Fyodor Soikin
    2020-11-20T06:08:05Z2020-11-20T06:08:05Z

    循环

    正如您非常正确地指出的那样,为了实现循环算法,递归用于函数式编程。例如:

    map f [] = []
    map f (x:xs) = f x : map f xs
    

    该函数map遍历整个列表并使用f. 很简单。

    然而,在实践中,很少直接使用递归。通常,函数式程序根据预先存在的库原语表达循环操作,例如map上面的函数。像这样的东西:

    numbers = [1,2,3,4,5]
    strings = show <$> numbers
    

    这里,操作符<$>是一个函数别名map(Haskell 中的标准),所以表达式show <$> numbers读作“将函数show应用于列表的每个元素numbers”。

    对于更复杂的情况,还有更通用的类比,例如fold,for等mapM。最简单的使用递归实现,而更复杂的则建立在最简单的之上。

    突变

    函数式编程(以及一般编程)中的“突变”是存储单元内容的变化。在 Haskell 中,突变原则上是可能的(见下文),但实际上他们试图避免它,只在最极端的情况下才使用它。在函数式编程中,通常不是直接更改数据,而是在旧结构的基础上创建新结构。例如:

    replaceFirst (x:xs) newX = newX : xs
    

    该函数replaceFirst将列表的第一个元素“替换”为一个新元素:

    xs = [1,2,3,4]
    ys = replaceFirst xs 42
    -- Теперь  ys = [42,2,3,4]
    

    (细心的读者会注意到该函数replaceFirst不适用于空列表,但这与当前讨论无关)

    从逻辑上讲,该操作replaceFirst xs 42可以理解为“将列表的第一个元素替换为xs” 42,但实际上并非如此。实际上,旧清单xs还活着,而且清单ys是一个全新的清单。现在我们有一个选择:我们可以将列表作为不必要的列表丢弃xs,以便垃圾收集器稍后将其删除,或者我们可以将其“保留”以备将来使用。

    这种方法开辟了很多可能性。例如,保留更改历史不会花费我们任何费用——无论是为了会计,还是为了及时回溯,或者其他什么。一个更有趣的可能性是程序并行化。如果我们确定旧数据永远不会改变,那么隔离信号量和其他同步是没有意义的——你可以并行运行程序,就是这样!- 编译器保证它们永远不会通过修改共享数据相互干扰。数据同步是并行编程的基石,也是 bug 的主要来源。如果有疑问,请寻找“防御性副本”。

    问:您是不是想说每次更改都必须分配一个全新的数据结构?!你不会得到足够的任何内存!

    不,它不需要。为了防止这种情况发生,函数式编程使用特殊的数据结构,在英文中称为“持久数据结构”。这些是数据结构,其中每个下一次更新都是在先前版本的基础上构建的。

    最简单的例子是单链表。想象一个列表:每个元素都包含一个指向前一个元素的链接(指针),而最近的元素包含一个指向“结束”的链接。像这样的东西:

    (1) --> (2) --> (3) --> (4) --> (end)
    

    在replaceFirst(见上文)的示例中,此列表是为我调用的xs:

         (1) --> (2) --> (3) --> (4) --> (end)
    xs _/
    

    当我调用该函数replaceFirst时,它获取了列表的“尾部”(从两个开始)并为其附加了一个新的“头部” 42。我称之为“新”列表ys:

     ys _
          \
          (42)_
                \
        (1) --> (2) --> (3) --> (4) --> (end)
    xs _/
    

    这表明为了“替换”列表的第一个元素,根本不需要分配新的内存块。唯一的新单元是新的第一个元素本身,仅此而已。所有其他元素都保持原样。此外,如果我现在决定 xs不再需要该列表,那么内存收集器将只需要删除 number 1,因为列表的所有其他元素仍然涉及。

    另请注意,这种方法只有在确定列表的元素永远不会更改时才有可能。只有在这种情况下,我才能将列表xs视为ys两个不同的列表,尽管它们部分占用相同的内存。

    当然,单链表是最简单的数据结构。在更复杂的情况下,使用更复杂的结构——主要是各种类型和大小的树。当然,在某些情况下,这种方法的效率仍然低于内存中的连续数组。但是,在 99% 的情况下,性能上的差异完全可以忽略不计,无法与增加的稳定性和易于开发性相提并论。

    然而,对于剩下的 1% 的情况,即使是 Haskell 也允许您使用连续数组 - 只有它以更安全的方式来处理(见下文)

    命令式程序

    “命令式”被称为程序,表示为一系列步骤。这与“声明性”形成对比,“声明性”表示为部分之间的关​​系 - 功能和数据。我上面给出的所有例子 - “声明性”。它们都以“”的样式编写xs- 这ys是第一个元素被42“替换的地方,依此类推。乍一看,似乎在原则上没有区别,但事实并非如此。主要区别在于命令式程序具有操作顺序,而声明性程序则没有。我已经说明了该值xs与 的关系ys,但我没有说明我希望以什么顺序评估这种关系。编译器会帮我解决的。

    但这并不总是可取的。尽管“原则上”任何程序都可以用两种方式表达,但从纯人类的角度来看,有时将程序视为功能关系,有时更方便,有时将其视为一系列步骤。

    例如,考虑相同的游戏。想象一下,我有一个特定的数据类型Game和一组操作:

    type Game = ...
    lookForMonsters :: Game -> (Game, Monster)
    shoot :: Monster -> Game -> (Game, Damage)
    

    请注意,每个函数除了其“主要”结果(Monster或Damage)外,还返回一个“新”的、修改后的游戏状态。这个“新”状态必须传递给下一个函数,因此这些函数有一定的执行顺序,如下所示:

    game0 = createGame
    (game1, monster) = lookForMonsters game0
    (game2, damage) = shoot monster game1
    message = "Inflicted " ++ show damage ++ " points of damage
    

    由于game1需要调用该值,shoot但它是 的结果lookForMonsters,因此lookForMonsters您需要先调用,然后shoot- 然后。这是调用顺序的定义。

    然而,这种风格非常乏味,并且会产生拼写错误的危险。另外,要注意图案:所有线条的形状都非常相似。函数式程序员以模式为生。他找到它们并无情地概括以使他的代码更简单、更易于理解。在这种情况下,让我们将这个模式包装在几个函数中,我将调用它们bind和return:

    bind x f = \game ->
        let (game1, y) = x game
        in f y game1
    
    return x = \game -> x
    

    如果不清楚,请不要详细说明。知道该函数bind将游戏上的两个操作“链接在一起”就足够了,将游戏的状态从第一个操作的结果传递给第二个操作的参数;并且该函数return创建一个忽略游戏状态并返回一些特定值的操作。使用这些函数,我可以这样编写程序:

    program = 
      bind lookForMonsters 
           (\monster ->
                bind (shoot monster) 
                     (\damage ->
                         return ("Inflicted " ++ show damage ++ " points of damage)
                     )
           )
    
    (finalGameState, message) = program game0
    

    你可以通过换行和缩进让它更漂亮一点:

    program = 
      bind lookForMonsters (\monster ->
      bind (shoot monster) (\damage ->
      return ("Inflicted " ++ show damage ++ " points of damage)))
    

    或者如果你有一个>>=将成为同义词的运算符,甚至更漂亮一点bind:

    x >>= f = bind x f
    
    program = 
      lookForMonsters >>= \monster ->
      shoot monster >>= \damage ->
      return ("Inflicted " ++ show damage ++ " points of damage)
    

    这种确定执行顺序的方案称为“monad”,它在函数式语言中应用如此广泛,以至于大多数编译器都为其提供了特殊的语法糖。在 Haskell 中,这个程序可以这样写:

    program = do
        monster <- lookForMonsters
        damage <- shoot monster
        return ("Inflicted " ++ show damage ++ " points of damage)
    

    单词do和左箭头被<-编译器翻译成连续的函数调用bind。

    在所有这些转换之后,看看我们得到了什么:程序看起来像 C 或 Python 等某种语言中的“常规”命令式程序,看起来我们每一步都在“改变”游戏状态。但实际上,该程序完全由没有变异和副作用的“纯”函数组成。我们的程序将游戏的初始状态作为输入并产生最终状态作为输出,并且不依赖于其他任何东西。因此,我们有机会以一系列步骤的形式编写程序,但同时保留了没有突变的所有优点。

    概括

    再看一下我上面用“棘手”的方式编写的程序:

    program = do
        monster <- lookForMonsters
        damage <- shoot monster
        return ("Inflicted " ++ show damage ++ " points of damage)
    

    请注意,这个程序的任何地方都没有提到类型本身Game——也就是说,程序本身通常不知道它与游戏一起工作。所有关于游戏的知识都“隐藏”在函数bind和原始操作lookForMonsters中shoot。顺便说一句,这些操作本身也可能不是原始的——它们本身可以用与我的程序相同的风格编写,并且基于更原始的操作。但这只是一个词。

    而接下来的想法是:如果程序本身不依赖于“运算顺序”的思想究竟是如何实现的(即这个思想是由函数实现的bind),那么我们可以改变这个在不触及程序本身的情况下实施。我们的实现可以简单且“玩具”,正如我上面给出的那样,或者如果我们需要一些额外的功能,例如错误中断或日志记录,我们可以将这些功能添加到函数bind中,程序不会注意到它。

    这种想法导致了“单子”可以不同的想法。此外,monad 可以从具有所需功能的块(如从多维数据集)中组装,同时编写程序,其中每个操作仅使用对其重要的多维数据集,而程序的其余部分不知道它们。

    输入输出

    而现在,当我们有了“monad”的概念,它是操作顺序的抽象表达,下面的想法就来了:如果我们发明一个不是用 Haskell 语言本身编写的,而是在编译器?如果你以这样一种方式组织这个 monad,使函数bind可以做各种耻辱,打破规则,并且通常是用 C 语言编写的呢?

    用这样的 monad 编写的程序看起来完全是无辜的 - 就像一系列通过的函数bind,就像上面的例子一样。但是原始操作将用于真正的输入输出。

    这是一个示例操作getLine:

    getLine :: IO String
    

    它在现实世界中具有“副作用”——它从控制台读取文本字符串。但是这个字符串在 type 中被“包装”了IO。从逻辑上讲,这可以被认为是字符串所在的单元格,但是没有办法从那里“获取”这个字符串。你唯一能做的就是通过调用将它作为参数传递给另一个函数bind。但问题是:作为这个调用的结果,类型将再次被确定出来IO(可能不是在里面有一个字符串,而是有别的东西),你又不能从那里得到值。您只能在帮助下再次将其发送到第三个函数bind- 依此类推。

    结果是,一旦我们完成了 I/O(即,调用其中一个IO-functions),唯一可以对结果做的就是做更多的 I/O,更多,更多。但这没关系,因为 Haskell 程序的入口点只是一个 I/O 操作链,通过bind. 例如:

    main = 
        bind getLine 
             (\name ->
                 bind (putStrLn ("Hello, " ++ name ++ "!"))
                      (\_ -> 
                          return 0
                      )
             )
    

    或使用相同的语法do:

    main = do
        name <- getLine
        putStrLn ("Hello, " ++ name ++ "!")
        return 0
    

    这就是 Haskell 中 I/O 的工作方式。最原始的操作,比如getLineand putStrLn,不是用 Haskell 自己写的,而是用 C 写的。这和CreateProcess在 Windows 或forkUNIX 中的调用差不多,不是由程序本身实现的,而是由操作系统实现的。最原始的原语总是比它低一层,无论如何都没有它。

    看待这种结构的另一种方法是,您可以将 Haskell 程序视为一个自身不执行 I/O 的程序,而是用另一种语言生成一个执行 I/O 的程序。我不是这种方法的忠实拥护者,但它经常用于解释。

    请注意,原则上,您完全可以直接在 中编写整个 Haskell 程序IO,从而享受与 C 或 Python 程序相同的“好处”。然而,这完全扼杀了整个想法。Haskell 的重点是,如果您摆脱所有类型的废话IO并让编译器优化程序,您可以编写更安全、更具表现力和更快的程序。相信我,编译器(尤其是 Haskell 编译器)的性能比你我好几个数量级。

    因此,真正的程序通常分为两层:程序的最核心,所有逻辑和所有计算都写在“纯”函数中,只有最外层,非常薄的外壳处理IO. 如今,这一原则在英语中被称为“功能核心,命令式外壳”,在函数式编程之外已经广为人知——事实上,许多其他的函数式思想(例如,LINQ、const、then、async、auto等)也广为人知。 P.)

    “真实”突变

    既然我们已经遇到了 monadIO并且了解到它的原语可以做各种乱七八糟的事情,接下来想到的是这个漏洞可以用于 Haskell 仍然不够快、需要太多内存或由于某些原因其他原因不适合。在这些情况下,您可以用 C 编写一个库并通过IO.

    这种方法的一个例子是具有“真实”突变支持的数组。该库提供IO了用于创建、修改和对数组进行其他操作的 -primitives:

     main = do 
         arr <- newArray (1,10) 42
         writeArray arr 8 5
         a <- readArray arr 1
         print a
    

    正如我上面提到的,这些特性只在最极端的情况下使用——当 Haskell 本身“不支持”时。我会将它与用 C 编写程序进行比较,但汇编程序散布在最敏感的地方。程序变得更大、更难以理解、更脆弱,并且限制了优化的可能性,因为编译器不知道这些原语内部到底发生了什么,因此害怕接触它们。

    • 32
  2. Mark Shevchenko
    2020-11-22T16:18:21Z2020-11-22T16:18:21Z

    现代程序员的悲剧在于,学过数学,开始编程,他们试图理解为什么记录

    x = x + 1
    

    有这个意思。当他们明白这一点时,他们就很难学会“像以前一样”思考。Dmitry Soshnikov 说,此时函数式程序员死在其中。

    不过,是可以记住的,也没有那么难。尝试在纸上解任何方程,你会以函数式的方式来做。尝试计算阶乘。你会发现你不需要累加器变量。

    累加器变量的一个例子是经典的算盘,并且确实出现了state这样的东西。

    函数式表示法是如此熟悉,以至于它出现在所有命令式语言中,除了一些深奥的语言。看任务

    g = a * b + c * d + e * f;
    

    告诉我,右边的乘法将以什么顺序执行?第一个a * b,最后一个e * f,反之亦然?a * b而c * d这些是操作符的操作数+,而在C、C++等语言中,操作数的求值顺序是没有定义的。因此,编译器可以优化代码。例如,如果上面某处c * d已经计算过了,并且变量的值没有改变,你可以用成品代替。

    未指定评估顺序的事实表明了功能样式。这就是他。如果你想以命令式的方式计算一个表达式,你必须把它分解成几个步骤,或者用 Fort 或 PostScript 编写它。

    最后,另一种体验函数式风格的方法是在 Excel 中编写计算。例如,不使用函数,SIN制作一张计算正弦的表格。这是一个有趣的任务。

    在这里,您也只定义了单元格之间的依赖关系,但没有定义计算它们的顺序。Excel 当然不是图灵完备的计算器,但是,它可以让您解决许多实际问题。

    • 15
  3. Вантик Иванов
    2020-11-25T22:03:03Z2020-11-25T22:03:03Z

    没有变量的编程是不可能的。首先,如果我们想在整个脚本中更改一个值,那么我们将不得不在整个程序中更改这个值(脚本中可能有数千、数万行)。其次,如果我们需要使用动作符号(+、-、*、:),那么我们就不能没有变量。

    • -2

相关问题

  • Scala 匿名函数简写

  • 为什么我们需要 Nothing、Null、Nil 和 None

  • 帮助在 Scala 中重写 LINQ 查询

  • Scala 中的模式匹配

Sidebar

Stats

  • 问题 10021
  • Answers 30001
  • 最佳答案 8000
  • 用户 6900
  • 常问
  • 回答
  • Marko Smith

    是否可以在 C++ 中继承类 <---> 结构?

    • 2 个回答
  • Marko Smith

    这种神经网络架构适合文本分类吗?

    • 1 个回答
  • Marko Smith

    为什么分配的工作方式不同?

    • 3 个回答
  • Marko Smith

    控制台中的光标坐标

    • 1 个回答
  • Marko Smith

    如何在 C++ 中删除类的实例?

    • 4 个回答
  • Marko Smith

    点是否属于线段的问题

    • 2 个回答
  • Marko Smith

    json结构错误

    • 1 个回答
  • Marko Smith

    ServiceWorker 中的“获取”事件

    • 1 个回答
  • Marko Smith

    c ++控制台应用程序exe文件[重复]

    • 1 个回答
  • Marko Smith

    按多列从sql表中选择

    • 1 个回答
  • Martin Hope
    Alexandr_TT 圣诞树动画 2020-12-23 00:38:08 +0000 UTC
  • Martin Hope
    Suvitruf - Andrei Apanasik 什么是空? 2020-08-21 01:48:09 +0000 UTC
  • Martin Hope
    Air 究竟是什么标识了网站访问者? 2020-11-03 15:49:20 +0000 UTC
  • Martin Hope
    Qwertiy 号码显示 9223372036854775807 2020-07-11 18:16:49 +0000 UTC
  • Martin Hope
    user216109 如何为黑客设下陷阱,或充分击退攻击? 2020-05-10 02:22:52 +0000 UTC
  • Martin Hope
    Qwertiy 并变成3个无穷大 2020-11-06 07:15:57 +0000 UTC
  • Martin Hope
    koks_rs 什么是样板代码? 2020-10-27 15:43:19 +0000 UTC
  • Martin Hope
    Sirop4ik 向 git 提交发布的正确方法是什么? 2020-10-05 00:02:00 +0000 UTC
  • Martin Hope
    faoxis 为什么在这么多示例中函数都称为 foo? 2020-08-15 04:42:49 +0000 UTC
  • Martin Hope
    Pavel Mayorov 如何从事件或回调函数中返回值?或者至少等他们完成。 2020-08-11 16:49:28 +0000 UTC

热门标签

javascript python java php c# c++ html android jquery mysql

Explore

  • 主页
  • 问题
    • 热门问题
    • 最新问题
  • 标签
  • 帮助

Footer

RError.com

关于我们

  • 关于我们
  • 联系我们

Legal Stuff

  • Privacy Policy

帮助

© 2023 RError.com All Rights Reserve   沪ICP备12040472号-5