【问题标题】:haskell - flip fix / fixhaskell - 翻转修复/修复
【发布时间】:2013-03-20 12:07:58
【问题描述】:
>>>flip fix (0 :: Int) (\a b -> putStrLn "abc")
Output: "abc"

这是使用flip fix的简化版本。
我在一些可能来自谷歌技术谈话或其他谈话的 youtube 视频中看到了这种使用方式。

谁能给我一些指针(不是一些内存地址,谢谢!)fix 到底是什么。我从官方网站上的文档中知道一般定义。而且我在互联网上浏览了很多东西,只是找不到一个全面且易于理解的答案。

flip fix 对我来说就像一个谜。在那个特定的函数调用中实际发生了什么?

顺便说一句,我是在 2 个月前才开始使用 Haskell。而且我数学不是很好:(


这是完整的代码,由演示者分享,如果有人感兴趣的话:

(哦,这里是解释游戏的维基链接mastermindClick

module Mastermind where

import Control.Monad
import Data.Function
import Data.List
import System.Random

data Score = Score
  { scoreRightPos :: Int
  , scoreWrongPos :: Int
  }
  deriving (Eq, Show)

instance Read Score where
  readsPrec _ r = [ (Score rp wp, t)
                  | (rp, s) <- readsPrec 11 r
                  , (wp, t) <- readsPrec 11 s
                  ]

calcScore :: (Eq a) => [a] -> [a] -> Score
calcScore secret guess = Score rightPos wrongPos
  where
    rightPos    = length [() | (a, b) <- zip secret guess, a == b]
    wrongPos    = length secret - length wrongTokens - rightPos
    wrongTokens = guess \\ secret

pool :: String
pool = "rgbywo"

universe :: [String]
universe = perms 4 pool

perms :: Int -> [a] -> [[a]]
perms n p = [s' | s <- subsequences p, length s == n, s' <- permutations s]

chooseSecret :: IO String
chooseSecret = do
  i <- randomRIO (0, length universe - 1)
  return $ universe !! i

guessSecret :: [Score] -> [String]-> [String]
guessSecret _      []    = []
guessSecret ~(s:h) (g:u) = g : guessSecret h [g' | g' <- u, calcScore g' g == s]

playSecreter :: IO ()
playSecreter = do
  secret <- chooseSecret
  flip fix (0 :: Int) $ \loop numGuesses -> do
    putStr "Guess: "
    guess <- getLine
    let
      score       = calcScore secret guess
      numGuesses' = numGuesses + 1
    print score
    case scoreRightPos score of
      4 -> putStrLn $ "Well done, you guessed in " ++ show numGuesses'
      _ -> loop numGuesses'

playBoth :: IO ()
playBoth = do
  secret <- chooseSecret
  let
    guesses     = guessSecret scores universe
    scores      = map (calcScore secret) guesses
    history     = zip guesses scores
  forM_ history $ \(guess, score) -> do
    putStr "Guess: "
    putStrLn guess
    print score
  putStrLn $ "Well done, you guessed in " ++ show (length history)

playGuesser :: IO ()
playGuesser = do
  input <- getContents
  let
    guesses     = guessSecret scores universe
    scores      = map read $ lines input
    history     = zip guesses scores
  forM_ guesses $ \guess -> do
    putStrLn guess
    putStr "Score: "
  case snd $ last history of
    Score 4 0 -> putStrLn $ "Well done me, I guessed in " ++ show (length history)
    _         -> putStrLn "Cheat!"

【问题讨论】:

  • 仅供参考,演讲是关于实施游戏 Mastermind,由伦敦 Haskell 用户组的 Peter Marks 提供。

标签: haskell functional-programming fixpoint-combinators


【解决方案1】:

fixfixed-point operator。正如您可能从它的定义中知道的那样,它计算函数的不动点。这意味着,对于给定的函数f,它会搜索一个值x,使得f x == x

如何为任意函数找到这样的值?

我们可以将x视为无限期f (f (f ... ) ...))的结果。显然,既然是无限的,在它前面加上f并不会改变它,所以f x会和x一样。当然,我们不能表达一个无限大的术语,但是我们可以将fix定义为fix f = f (fix f),表达思想。

有意义吗?

它会终止吗?是的,它会的,但这只是因为 Haskell 是一种惰性语言。如果f 不需要它的参数,它不会计算它,所以计算将终止,它不会永远循环。如果我们在一个总是使用其参数(它是严格的)的函数上调用fix,它将永远不会终止。所以有些函数有一个固定点,有些没有。 Haskell 的惰性求值确保我们计算它(如果存在)。

为什么fix 有用?

它表示递归。任何递归函数都可以使用fix 表示,无需任何额外的递归。所以fix是一个非常强大的工具!假设我们有

fact :: Int -> Int
fact 0 = 1
fact n = n * fact (n - 1)

我们可以使用fix 消除递归,如下所示:

fact :: Int -> Int
fact = fix fact'
  where
    fact' :: (Int -> Int) -> Int -> Int
    fact' _ 0 = 1
    fact' r n = n * r (n - 1)

这里,fact' 不是递归的。递归已移至fix。这个想法是fact' 接受一个函数作为其第一个参数,如果需要,它将用于递归调用。如果您使用fix 的定义扩展fix fact',您会发现它与原始fact 的作用相同。

因此,您可以拥有一种仅具有原始 fix 运算符并且不允许任何递归定义的语言,并且您可以使用递归定义来表达您可以表达的一切。

回到你的例子

让我们查看flip fix (0 :: Int) (\a b -&gt; putStrLn "abc"),它只是fix (\a b -&gt; putStrLn "abc") (0 :: Int)。现在让我们评估一下:

fix (\a b -> putStrLn "abc") =
(\a b -> putStrLn "abc") (fix (\a b -> putStrLn "abc")) =
\b -> putStrLn "abc"

所以整个表达式的计算结果为(\b -&gt; putStrLn "abc") (0 :: Int),即putStrLn "abc"。因为函数\a b -&gt; putStrLn "abc" 忽略了它的第一个参数,所以fix 永远不会递归。它实际上在这里仅用于混淆代码。

【讨论】:

  • 太棒了!当我看到你的解释时,我恰好正在观看另一个关于懒惰的视频,演讲者是西蒙佩顿琼斯!为胜利而懒惰。我不知道fix 可以终止只是因为它是 Haskell!
  • 所以fact'是它自己的第一个参数,Int参数(第一个模式匹配中的0n第二个模式匹配)与fact 的唯一(省略)参数相同。是对的吗? @Petr Pudlák
  • @prM fact' 的第一个参数实际上是fix fact'。我们对fact' 说类似“计算一级计算,如果需要,我们会为您提供您自己的递归版本”。函数fact' 的类型是(Int -&gt; Int) -&gt; (Int -&gt; Int),当我们在它上面使用fix 时,我们计算它的类型为(Int -&gt; Int) 的不动点。所以定点结果是一个函数!这就是我们只有fix fact' 的原因。你是对的,fact' 的第二个 Int 参数对应于 fact 的唯一参数。
  • @prM 维基百科文章Fixed-point combinator 也提供了有价值的信息。如果您也在研究 lambda 演算,您可能会对 Clear, intuitive derivation of the fixed-point combinator (Y combinator)? 感兴趣。 Y 组合子与fix 基本相同,只是用无类型的 lambda 演算表示。 (在 Haskell 中,我们将 fix 定义为递归函数,但在无类型 lambda 演算中,我们可以将此运算符定义为 lambda 项,无需任何递归。)
  • 优秀的答案。但是,我要强调两点:(a) 值得明确指出 fact'fact' _ 0 = 1 的第一个等式不使用它的第一个参数,这就是 fact' (fact' (...)) 无限堆栈是如何“退出的” “ 在这种情况下; (b) 虽然fix f = f (fix f) 的定义更容易理解,但更实用的替代方案是fix f = let r = f r in r,它对性能更友好;容易编译的代码往往会在每一步都分配一个新的 thunk,而 let 版本会导致循环引用图重用相同的 thunk。
【解决方案2】:

这只是编写递归 lambda 的一种有趣方式,我可以想到这样做的两种可能性:

  • 程序员想迷惑新手。
  • 他来自一种对递归有更多限制的语言(比如某些 LISP,或者可能是 ML?)

您可以更清晰地重写代码:

    loop secret 0
where
    loop secret numGuesses = do
         putStr "Guess: "
         guess <- getLine
         let
             score       = calcScore secret guess
             numGuesses' = numGuesses + 1
         print score
         case scoreRightPos score of
           4 -> putStrLn $ "Well done, you guessed in " ++ show numGuesses'
           _ -> loop secret numGuesses'

不同之处在于您必须手动传递 secret,而递归 lambda 可以避免这种情况(这可能是使用 fix 编写它的另一个原因)

为了更深入地了解修复,谷歌搜索“y-combinator”

【讨论】:

    猜你喜欢
    • 2014-03-08
    • 1970-01-01
    • 2017-10-20
    • 2018-10-03
    • 2017-04-03
    • 2017-10-27
    • 2017-05-31
    • 2016-06-15
    • 2019-02-22
    相关资源
    最近更新 更多