【问题标题】:Implementing functional addition in Haskell在 Haskell 中实现函数加法
【发布时间】:2017-12-26 01:15:22
【问题描述】:

我在 Haskell 中得到了一个谜题,

f 有两个函数,函数a 和函数b。函数 a 接受 na 输入并返回 Num 类型,函数 b 接受 nb 输入并返回Num 类型。 f 返回一个新的 na+nb 函数,它将 a 应用于第一个 n a 个参数,nb 到其余参数并返回它们的总和。

在数学中我会这样写:

我在 Haskell 中第一次天真的尝试是:

f a b = flip ((+) . a) . b

但这只有在 a 是一元函数时才有效。

在这之后,我想了很久这个谜题,甚至无法想出我应该如何做到这一点的想法。这是很长时间以来我第一次对 Haskell 感到如此困惑。

我该如何解决这个难题?这个谜题有解决方案吗? (我的一个朋友给了我这个谜题,我不相信他们当时有真正的解决方案)

【问题讨论】:

  • 我们可以在 Haskell 中举个例子吗,因为在 Haskell 中有两种解释 $f(a,b)$ 的方法(即你想要 f a b :: (Int,Int,Int) -> Int 还是 f a b :: Int -> Int -> Int?)
  • 采用n_a 输入的函数类型为Vec n_a X -> Y 其中data Vec n a where Nil :: Vec 0 a; Cons ::a -> Vec n a -> Vec (1 + n) a
  • @DanRobertson 严格来说,我们不知道f a b 有这两种类型,因为f a b 可能接受任意数量的输入。但是我认为这个谜题的目的是为了一个柯里化函数,因为在 Haskell 中拥有一个任意大小的元组显然是不可能的。 (话虽这么说,以这种方式进行柯里化可能是不可能的,我只是不确定,因为我确信采用任意元组是行不通的)
  • 当然可以。我使用了一个任意示例来说明您是否想要柯里化或类似上面的Vec 或某种类型的同质元组列表混合。这样就搞定了。
  • @DanRobertson 这个谜题是按原样给我的,所以我会对这两种/任何类型的解决方案都感兴趣。柯里化很酷,但解决方案就是解决方案,我当然不会抱怨,因为我发现这个谜题很有趣,新的思维方式可能对我有好处。

标签: haskell functional-programming


【解决方案1】:

这是一种非常简单的方法,它使用在数字类型中单态工作的类型族(例如,专用于 Int)。我们需要一些扩展:

{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE InstanceSigs #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TypeFamilies #-}

函数f将定义在一个类型类中:

class VarArgs r s where
  type F r s
  f :: r -> s -> F r s

我们将处理以下情况。如果第一个函数的类型是 a :: Int -> r 形式,我们将使用以下实例来吞噬参数 x 并将其提供给 a

instance VarArgs r s => VarArgs (Int -> r) s where
  type F (Int -> r) s = Int -> F r s
  f :: (Int -> r) -> s -> Int -> F r s
  f a b x = f (a x) b

这具有递归 a 的类型的效果,直到它的形式为 Int。然后,我们将使用类似的实例来递归 b :: Int -> s 类型:

instance VarArgs Int s => VarArgs Int (Int -> s) where
  type F Int (Int -> s) = Int -> F Int s
  f :: Int -> (Int -> s) -> Int -> F Int s
  f a b x = f a (b x)

最终,这两个函数都将简化为a, b :: Int类型的0元函数,我们可以使用终端实例:

instance VarArgs Int Int where
  type F Int Int = Int
  f :: Int -> Int -> Int
  f a b = a + b

这里有一个小测试来证明它有效:

times2 :: Int -> Int -> Int
times2 x y = x * y
times3 :: Int -> Int -> Int -> Int
times3 x y z = x * y * z

foo :: [Int]
foo = [ f times2 times2 1 2 3 4
      , f times2 times3 1 2 3 4 5
      , f times3 times2 1 2 3 4 5
      , f times3 times3 1 2 3 4 5 6]

并将其加载到 GHCi 中会得到:

> foo
[14,62,26,126]
>

将其概括为任何Num 类型的多态似乎并不简单。将 Int 类型替换为受约束的 Num n 类型会导致有关冲突族实例声明的错误。

【讨论】:

  • 赞成,但我不会称其为“非常简单的方法”;-) 这并不难,但不完全适合初学者。
  • 另一个小问题是你需要 times2。为什么不只是(*)f (*) (*) 1 2 3 4 会产生一连串错误,因为(*) 的类型对f 来说太好了。
  • 我怀疑你最好数箭而不是寻找Int。您会错过函数的 Num 实例,但会抓住其余的。
【解决方案2】:

这很简单——在我看来,这比 @K.A.Buhr 的类型族方法要简单得多——如果你调整 n 元函数的表示,而不是使用 n 元函数的一元函数em>n 维向量。

{-# LANGUAGE GADTs #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}

import Prelude hiding (splitAt)
import Data.Bifunctor

常见的怀疑对象:(类型级)自然数、它们的(值级)单例、类型级加法和向量。

data Nat = Z | S Nat
data Natty n where
    Zy :: Natty Z
    Sy :: Natty n -> Natty (S n)
type family n + m where
    Z + m = m
    S n + m = S (n + m)

data Vec n a where
    Nil :: Vec Z a
    (:>) :: a -> Vec n a -> Vec (S n) a

splitAt 需要运行时 Natty - 它需要在运行时知道在哪里分割向量 - 以及至少与 Natty 一样长的向量。

splitAt :: Natty n -> Vec (n + m) a -> (Vec n a, Vec m a)
splitAt Zy xs = (Nil, xs)
splitAt (Sy n) (x :> xs) =
    let (ys, zs) = splitAt n xs
    in (x :> ys, zs)

那么您的f(我称之为splitApply)是splitAt 的直接应用。

splitApply :: Natty n -> (Vec n a -> b) -> (Vec m a -> c) -> Vec (n + m) a -> (b, c)
splitApply at f g xs = bimap f g $ splitAt at xs

(我没有费心展示“添加结果”部分,因为它太简单了,以至于我写起来很无聊。你可以争辩说,因为 Hask 是一个幺半群类别,@ 987654331@ 无论如何都代表一种加法。)

【讨论】:

  • 我认为HList 可能比向量更适合——问题描述并不表明函数参数都具有相同的类型。整个事情的结果大致相同,除了你有++而不是+。但是为了获得最大的类型推断,您可能还需要定义从右侧和左侧的丢弃。
猜你喜欢
  • 2021-12-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-01-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多