如何返回带有守卫和双重递归的 lambda?

Posted

技术标签:

【中文标题】如何返回带有守卫和双重递归的 lambda?【英文标题】:How can I return a lambda with guards and double recursion? 【发布时间】:2022-01-05 20:54:57 【问题描述】:

我用 Python 制作了这个函数:

def calc(a): return lambda op: 
    '+': lambda b: calc(a+b),
    '-': lambda b: calc(a-b),
    '=': a[op]

所以你可以这样计算:

calc(1)("+")(1)("+")(10)("-")(7)("=")

结果将是5

我想在 Haskell 中创建相同的函数来了解 lambda,但我遇到了解析错误。

我的代码如下所示:

calc :: Int -> (String -> Int)
calc a = \ op 
    | op == "+" = \ b calc a+b
    | op == "-" = \ b calc a+b
    | op == "=" = a

main = calc 1 "+" 1 "+" 10 "-" 7 "="

【问题讨论】:

你使用什么haskell资源?您发布的内容毫无意义。 我只是在谷歌上搜索haskell return lambda并即兴创作。我希望 python 示例有助于使其更有意义。 为什么对尝试学习 Haskell 的人没有意义?与其用笼统的语言批评他们,不如向他们解释为什么这在 H​​askell 中是不可能的?是不是因为 Haskell 是强类型的,并且不允许像 Python 那样为一个函数提供 2 种不同的返回类型?有什么解决方法吗?我是 Haskell 的新手,对这个问题感到好奇 @MichaelLitchard 问题中的代码对 Haskell 编译器毫无意义,当然,但人类可以看到它是如何尝试复制 Python 代码并解释为什么它是错误的。 非常好!谢谢你的慰问。 :) 【参考方案1】:

您发布的代码存在许多语法问题。不过,我不会在这里讨论它们:在完成基本的 Haskell 教程后,您将自己发现它们。相反,我将专注于该项目的一个更基本的问题,即类型并不能真正发挥作用。然后,我将展示一种不同的方法,它可以为您带来相同的结果,向您展示一旦您了解了更多信息,它在 Haskell 中是可能的。

虽然在 Python 中有时返回一个 int 函数,有时返回一个 int 很好,但在 Haskell 中这是不允许的。 GHC 必须知道在编译时会返回什么类型;您无法在运行时根据字符串是否为"=" 做出该决定。因此,“keep calcing”参数需要与“给我答案”参数不同的类型。

这在 Haskell 中是可能的,实际上是一种具有很多应用程序的技术,但它可能不是初学者的最佳起点。您正在发明延续。您希望 calc 1 plus 1 plus 10 minus 7 equals 生成 5,用于其中使用的名称的一些定义。实现这一点需要 Haskell 语言的一些高级特性和一些有趣的类型1,这就是为什么我说它不适合初学者。但是,下面是满足此目标的实现。我不会详细解释它,因为你要先学习的东西太多了。希望在学习了一些 Haskell 基础知识之后,您可以回到这个有趣的问题并理解我的解决方案。

calc :: a -> (a -> r) -> r
calc x k = k x

equals :: a -> a
equals = id

lift2 :: (a -> a -> a) -> a -> a -> (a -> r) -> r
lift2 f x y = calc (f x y)

plus :: Num a => a -> a -> (a -> r) -> r
plus = lift2 (+)

minus :: Num a => a -> a -> (a -> r) -> r
minus = lift2 (-)
ghci> calc 1 plus 1 plus 10 minus 7 equals
5

1 当然calc 1 plus 1 plus 10 minus 7 equals 看起来很像1 + 1 + 10 - 7,这非常简单。这里的重要区别是这些是中缀运算符,因此被解析为(((1 + 1) + 10) - 7),而您尝试在 Python 中实现的版本和我的 Haskell 解决方案被解析为 ((((((((calc 1) plus) 1) plus) 10) minus) 7) equals) - 没有偷偷摸摸的中缀运算符,并且calc 控制所有组合。

【讨论】:

这与 C. Okasaki 在“在 Haskell 中嵌入后缀语言的技术”中所做的非常相似 经过快速实验,我看到在 GHC 8 中 Bool -> forall a. Maybe aforall a. Bool -> Maybe a 统一,但在 GHC 9 中不再是这种情况。我猜 foralls 不再被提升。奇怪的是,即使在 GHC8 中,类型参数也需要在正确的位置传递 f True @Tf @T True 我可以在 9.2 中重现它:代码构建良好,但 GHCi 演示仅使用 ImpredicativeTypes 运行。这几乎肯定与simplified subsumption 有关。特别是,如果我们要对所有内容进行 eta 扩展,则不需要扩展:calc 1 $ \x -> plus x 1 $ \x -> plus x 10 $ \x -> minus x 7 equals。我猜ImpredicativeTypes 会有所帮助,因为它支持的快速查看推理能够在这里找出正确的做法。 @duplode 我发现了一种比这更简单的方法:只需去掉 Calc 类型的同义词,而是在各处写下 (a -> r) -> r。这还有一个好处,就是您不再需要启用任何扩展。 @JosephSible-ReinstateMonica 一个折中的解决方案可能是保留同义词但不隐藏r (type Calc r a = (a -> r) -> r)。【参考方案2】:

A -> B 类型的 Haskell 函数每次被调用时都必须返回一个固定类型 B 的值(或者无法终止,或者抛出异常,但让我们忽略它吧)。

Python 函数没有类似的约束。返回的值可以是任何东西,没有类型限制。举个简单的例子,考虑:

def foo(b):
   if b:
      return 42        # int
   else:
      return "hello"   # str

在您发布的 Python 代码中,您利用此功能使 calc(a)(op) 成为函数(lambda)或整数。

在 Haskell 中我们不能这样做。这是为了确保可以在编译时对代码进行类型检查。如果我们写

bar :: String -> Int
bar s = foo (reverse (reverse s) == s)

不能期望编译器验证参数的计算结果是否始终为True——这通常是无法确定的。编译器只要求 foo 的类型类似于Bool -> Int。但是,我们不能将该类型分配给上面显示的foo 的定义。

那么,我们实际上可以在 Haskell 中做什么?

一种选择可能是滥用类型类。 Haskell 中有一种方法可以利用某种复杂的类型类机制来创建一种“可变参数”函数。那会让

calc 1 "+" 1 "+" 10 "-" 7 :: Int

类型检查并评估想要的结果。我没有尝试这样做:至少在我看来,它很复杂且“骇人听闻”。这个 hack 被用来在 Haskell 中实现 printf,它读起来并不好看。

另一种选择是创建自定义数据类型并在调用语法中添加一些中缀运算符。这也利用了 Haskell 的一些高级特性来对所有内容进行类型检查。

-# LANGUAGE GADTs, FunctionalDependencies, TypeFamilies, FlexibleInstances #-

data R t where
   I :: Int -> R String
   F :: (Int -> Int) -> R Int

instance Show (R String) where
    show (I i) = show i

type family Other a where
   Other String = Int
   Other Int    = String

(#) :: R a -> a -> R (Other a)
I i # "+" = F (i+)   -- equivalent to F (\x -> i + x)
I i # "-" = F (i-)   -- equivalent to F (\x -> i - x)
F f # i   = I (f i)
I _ # s   = error $ "unsupported operator " ++ s

main :: IO ()
main =
   print (I 1 # "+" # 1 # "+" # 10 # "-" # 7)

最后一行按预期打印5

关键思想是:

R a 类型表示中间结果,可以是整数或函数。如果它是一个整数,我们记得该行中的下一个内容应该是一个字符串,方法是创建I i :: R String。如果它是一个函数,我们记得下一个应该是一个整数,有F (\x -> ...) :: R Int

运算符(#) 获取R a 类型的中间结果,下一个“事物”(int 或字符串)到a 类型的进程,并产生一个“其他类型”Other a 的值。这里,Other a 定义为 Int 类型(分别为 String),而 aString(分别为 Int)。

【讨论】:

【参考方案3】:

chi's answer 说你可以用“复杂的类型类机器”来做到这一点,就像printf 一样。以下是你的做法:

-# LANGUAGE ExtendedDefaultRules #-

class CalcType r where
    calc :: Integer -> String -> r

instance CalcType r => CalcType (Integer -> String -> r) where
    calc a op
        | op == "+" = \ b -> calc (a+b)
        | op == "-" = \ b -> calc (a-b)

instance CalcType Integer where
    calc a op
        | op == "=" = a

result :: Integer
result = calc 1 "+" 1 "+" 10 "-" 7 "="

main :: IO ()
main = print result

如果你想让它更安全,你可以使用MaybeEither 来消除偏见,就像这样:

-# LANGUAGE ExtendedDefaultRules #-

class CalcType r where
    calcImpl :: Either String Integer -> String -> r

instance CalcType r => CalcType (Integer -> String -> r) where
    calcImpl a op
        | op == "+" = \ b -> calcImpl (fmap (+ b) a)
        | op == "-" = \ b -> calcImpl (fmap (subtract b) a)
        | otherwise = \ b -> calcImpl (Left ("Invalid intermediate operator " ++ op))

instance CalcType (Either String Integer) where
    calcImpl a op
        | op == "=" = a
        | otherwise = Left ("Invalid final operator " ++ op)

calc :: CalcType r => Integer -> String -> r
calc = calcImpl . Right

result :: Either String Integer
result = calc 1 "+" 1 "+" 10 "-" 7 "="

main :: IO ()
main = print result

这是相当脆弱的,非常不推荐用于生产用途,但无论如何它就像(最终?)学习的东西。

【讨论】:

它远非漂亮,但我预计它会更糟。尾随"=" 似乎有帮助。 printf 没有这样的“奢侈品”,如果我们可以这样称呼的话。 (+1)【参考方案4】:

这是一个 simple 解决方案,我想说它比其他答案中的高级解决方案更接近您的 Python 代码。这不是一个惯用解决方案,因为就像您的 Python 解决方案一样,它会使用运行时故障而不是编译器中的类型。

所以,Python 的本质是:返回一个函数或一个 int。在 Haskell 中,不可能根据运行时值返回不同的类型,但是 可以返回可以包含不同数据的类型,包括函数。

data CalcResult = ContinCalc (Int -> String -> CalcResult)
                | FinalResult Int

calc :: Int -> String -> CalcResult
calc a "+" = ContinCalc $ \b -> calc (a+b)
calc a "-" = ContinCalc $ \b -> calc (a-b)
calc a "=" = FinalResult a

出于最后会变得清楚的原因,我实际上会提出以下变体,它与典型的 Haskell 不同,不是 curried:

calc :: (Int, String) -> CalcResult
calc (a,"+") = ContinCalc $ \b op -> calc (a+b,op)
calc (a,"-") = ContinCalc $ \b op -> calc (a-b,op)
calc (a,"=") = FinalResult a

现在,您不能只在此之上堆放函数应用程序,因为结果绝不只是一个函数——它只能是一个包装函数。因为应用比处理它们的函数更多的参数似乎是失败的情况,所以结果应该在Maybe monad 中。

contin :: CalcResult -> (Int, String) -> Maybe CalcResult
contin (ContinCalc f) (i,op) = Just $ f i op
contin (FinalResult _) _ = Nothing

为了打印最终结果,让我们定义

printCalcRes :: Maybe CalcResult -> IO ()
printCalcRes (Just (FinalResult r)) = print r
printCalcRes (Just _) = fail "Calculation incomplete"
printCalcRes Nothing = fail "Applied too many arguments"

现在我们可以做

ghci> printCalcRes $ contin (calc (1,"+")) (2,"=")
3

好的,但是对于更长的计算,这将变得非常尴尬。请注意,在两次操作之后我们有一个Maybe CalcResult,所以我们不能再次使用contin。此外,需要向外匹配的括号很麻烦。

幸运的是,Haskell 不是 Lisp 并且支持中缀运算符。而且因为我们无论如何都会在结果中得到Maybe,所以不妨在数据类型中包含失败案例。

那么,完整的解决方案是这样的:

data CalcResult = ContinCalc ((Int,String) -> CalcResult)
                | FinalResult Int
                | TooManyArguments

calc :: (Int, String) -> CalcResult
calc (a,"+") = ContinCalc $ \(b,op) -> calc (a+b,op)
calc (a,"-") = ContinCalc $ \(b,op) -> calc (a-b,op)
calc (a,"=") = FinalResult a

infixl 9 #
(#) :: CalcResult -> (Int, String) -> CalcResult
ContinCalc f # args = f args
_ # _ = TooManyArguments

printCalcRes :: CalcResult -> IO ()
printCalcRes (FinalResult r) = print r
printCalcRes (ContinCalc _) = fail "Calculation incomplete"
printCalcRes TooManyArguments = fail "Applied too many arguments"

这允许你写

ghci> printCalcRes $ calc (1,"+") # (2,"+") # (3,"-") # (4,"=")
2

【讨论】:

我想知道为 CalcResult 实施 Applicative 是否会导致更多的 Haskellesque 使用? @user1984 CalcResult 不能是 Applicative,因为它没有类型参数。也许它有一些有意义的概括,即 Applicative,但没有什么能引起我的注意。 @amalloy 哦,对了!完全忘记了他们需要更高的善良(如果这是正确的术语的话。我的意思是善良* -> *):D

以上是关于如何返回带有守卫和双重递归的 lambda?的主要内容,如果未能解决你的问题,请参考以下文章

python匿名函数和递归

Python--lambda&递归

18.07.20(lambda().sorted().filter().map().递归.二分查找)

递归 lambda 返回时的 gcc 4.9 内部编译器错误

递归,二分法,lambda,filter,map,sorted

在对列表进行双重递归时,“预检查”是避免添加无类型或空字符串的首选方法吗?