【问题标题】:Haskell: a -> a -> ... -> b to [a] -> b [duplicate]Haskell:a -> a -> ... -> b 到 [a] -> b [重复]
【发布时间】:2016-02-23 10:54:36
【问题描述】:

我正在尝试将以下映射表示为 Haskell 函数:

给定两种类型 a, b 考虑函数族 F(a, b) 由类型的函数组成

f :: a -> a -> ... -> a -> b

n 重复 a,其中 n 是大于零的整数。 我想要的是将F(a, b) 中的每个函数f 映射到函数f' :: [a] -> b,这样f x1 x2 ... xr = f' [x1, ..., xr],其中r 小于f 需要的参数数量(即我正在寻找函数listify :: F(a, b) -> ([a] -> b))。如果元素多于 f 接受参数,则应丢弃额外的元素:

f :: a -> a -> b
(listify f xs) == (listify f $ take 2 xs)

此外,如果列出的为空,则任何值都可以接受。

我当然可以为具有固定数量参数的函数实现此映射(例如:listify :: (a -> a -> b) -> ([a] -> b) 等),但我找不到一种方法来编写一个对所有人都适用的函数f 同时在 F(a, b) 中。尽管 Template Haskell 可能能够为我提供正确的工具,但我对这样的解决方案不感兴趣。我想找到一些纯粹的“类型魔术”方式来做到这一点。

有人知道这是否可能吗?有人可以指出我正确的方向吗?或者这是一个已知的“问题”,已经解决了数十亿次,而我只是找不到解决方案?

【问题讨论】:

  • 你可以用一些OverlappingInstances 技巧来做到这一点(也许即使是争议较少的扩展),但我怀疑这是一个好主意。为什么不直接使用 list-accepting 函数呢?
  • 关于“为什么?”:这是否可能的问题只是突然出现在我的脑海中 --> 我问的是教育原因(也许还学习了一些新的 Haskell 魔法,同时试图弄清楚出一个解决方案)。我对您指的是哪个“列表接受功能”感到有些困惑。我有一个函数 f :: a -> a -> ... a -> b(带有未知数量的 'a')并想要一个函数 f' :: [a] -> b,这样 f x1 .. . xr = f' [x1, ...., xr]。因此,我没有列表接受功能,我想要一个!但可能我没有明白你真正的意思。
  • 您究竟会如何处理OverlappingInstances 以使其发挥作用?我看不出有什么办法。
  • 嗯,是的,有可能,这基本上是像printf 这样的可变参数签名的双重问题。然而,这种多态性往往会导致问题。特别是,您的建议绕过了编译器的类型检查:通过推迟从运行时长度列表中获取固定数量的参数,编译器无法检查是否有足够的参数,因此您需要一个合理的错误案例...我可以看到这个有用的应用程序是解析,但最好使用 解析器组合器
  • 有了“小论点”,你当然是对的。我只是想“嗯......只返回一个部分应用的函数。”。但是我刚才看到,这当然不会进行类型检查(甚至可能吗?我的意思是如果列表太短,则返回部分应用的函数)。关于有用性:我并不真正关心这种功能的应用程序/有用性/危险。我只是对如何去做感兴趣(我希望获得一些熟悉的 Haskell 知识 + 我认为 listify 很有趣;))。

标签: haskell types higher-order-functions


【解决方案1】:

我们只需要在这里挑选毒药。如果我们使用不太安全的 pragma,我们可以获得更多的推理,反之亦然;有很多组合。

第一个:使用重叠实例,将非函数作为基本情况,但无法处理多态a 类型:

{-# LANGUAGE MultiParamTypeClasses, TypeFamilies, FlexibleInstances #-}

class Listify a b where
  listify :: a -> b  

instance {-# OVERLAPS #-} r ~ ([a] -> b) => Listify b r where
  listify = const

instance (Listify f r, r ~ ([a] -> b)) => Listify (a -> f) r where
  listify f (a:as) = listify (f a) as

-- listify (+) [0, 2] -- error
-- listify (+) [0, 2 :: Int] -- OK
-- listify () [] -- OK

第二种:使用重叠实例,具有作为基本情况的函数,可以处理多态类型:

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

class Listify a b where
  listify :: a -> b  

instance {-# OVERLAPS #-} r ~ ([a] -> b) => Listify (a -> b) r where
  listify f (a:_) = f a

instance (Listify (a -> b) r, r ~ ([a] -> b)) => Listify (a -> a -> b) r where
  listify f (a:as) = listify (f a) as

-- listify (+) [0, 2] -- OK
-- listify () [] -- error, first arg must be a function

第三:使用不连贯的实例,在基本情况下具有值,可以处理多态类型:

{-# LANGUAGE MultiParamTypeClasses, TypeFamilies, FlexibleInstances #-}

class Listify a b where
  listify :: a -> b  

instance {-# INCOHERENT #-} r ~ ([a] -> b) => Listify b r where
  listify = const

instance (Listify f r, r ~ ([a] -> b)) => Listify (a -> f) r where
  listify f (a:as) = listify (f a) as

-- listify 0 [] -- OK
-- listify (+) [2, 4] -- OK

第四种:使用带有UndecidableInstances 的封闭类型族作为实例解析的帮助器,具有基本情况下的值,无法处理多态类型:

{-# LANGUAGE UndecidableInstances, ScopedTypeVariables, DataKinds,
    TypeFamilies, MultiParamTypeClasses, FlexibleInstances, FlexibleContexts #-}

import Data.Proxy

data Nat = Z | S Nat

type family Arity f where
  Arity (a -> b) = S (Arity b)
  Arity b        = Z

class Listify (n :: Nat) a b where
  listify' :: Proxy n -> a -> b

instance r ~ (a -> b) => Listify Z b r where
  listify' _ = const

instance (Listify n f r, a ~ (a' -> f), r ~ ([a'] -> b)) => Listify (S n) a r where
  listify' _ f (a:as) = listify' (Proxy :: Proxy n) (f a) as

listify :: forall a b. Listify (Arity a) a b => a -> b
listify = listify' (Proxy :: Proxy (Arity a))

-- listify (+) [3, 4] -- error
-- listify (+) [3, 4::Int] -- OK
-- listify () [] -- OK
-- listify 0 [] -- error
-- listify (0 :: Int) [] -- OK

在我看来,除了INCOHERENT 之外,大致上这些是人们可以在野外看到的变体,因为这在库代码中极为罕见(有充分的理由)。

我个人推荐带有封闭类型族的版本,因为UndecidableInstances 和类型族作为语言扩展目前争议最小,而且它们仍然提供了相当多的可用性。

【讨论】:

  • 尤其是最后一张很酷!这与我想象的非常接近(我能看到的唯一“缺陷”是显式类型注释)。这里有什么习俗?如果我认为您的答案比之前接受的更好,我是否允许/应该接受您的答案?
  • @morris:是的,你应该接受最好的答案,而不是最早的。
  • @morris:请注意,在上述解决方案中,如果参数类型是多态的,我们只需要类型注释(不连贯的实例除外,因为 GHC 会通过任何一种方式蠕动)。在示例中,数字字面量具有多态类型 Num a => a,但它始终与 Bool 无注释一起使用。
  • @AndrásKovács :您认为可以将部分应用程序合并到listify 中吗?我的意思是 listify (+) [1] 之类的东西(应该等于 (+) 1)。
  • @morris 作为一般规则,您应该避免立即接受答案。等待一段时间,甚至1天。原因很简单:一旦一个答案被接受,收到其他答案的机会就会下降。但是您通常不知道是否有可能比当前的答案更好。
【解决方案2】:

其实很简单,甚至不需要重叠实例:

{-# LANGUAGE MultiParamTypeClasses, FlexibleInstances #-}

class Listifiable f a b where
  listify :: f -> [a] -> b

instance Listifiable b a b where
  listify = const

instance (Listifiable f) a b => Listifiable (a->f) a b where
  listify f (x:xs) = listify (f x) xs

那你就可以了

GHCi> listify ((+) :: Int->Int->Int) [1,2 :: Int] :: Int
3

但是对那些本地显式类型签名的需求很能说明你遇到的问题。

FunctionalDependencies 或许可以缓解这个问题,但至少不是直接的方式。)

【讨论】:

  • 感谢您的解决方案! “需要那些本地显式类型签名”是的,这很丑陋。你知道是否有更好的方法来做到这一点?我的意思是f x1 ... xr = f' [x1, ..., xr] 的想法是合理的(即使考虑到部分应用)。如果在 Haskell 中没有办法“很好地”做到这一点,这将是我遇到的“数学上很容易表达,但很难实现”的第一个案例(非常难的意思:要么危险,要么完全丑陋)。您可能知道一种可以轻松安全地表达它的语言吗?
  • 问题在于参数数量不同的函数具有不同的类型,而不同长度的列表具有相同的类型。类型只存在于编译时,列表长度只存在于运行时。 — 而且,不,我不同意这是“数学上容易表达的”:什么是 ff' 在你的陈述中?你真正的意思是,它可以很容易地用动态类型的语言来表达。嗯,你可以easily implement a dynamic language in Haskell!但是你失去了静态类型的所有好处。
  • 我可以如下表达。令 X, Y 为非空集,将 Fn 直观地定义为 Fn := {X -> F(n-1)},其中 F1 := {X -> Y}。令 F 为 n 的所有 Fn 的并集,自然数大于 0。此外,考虑集合 L := (union {n = 0, to inf.} X^n) union {N -> X},其中 N是自然数 (L "=" {[X]})。 f in F 意味着 f in An 对于一些 n。定义 f' : L -> (union {n = 1, to inf.} An) union Y 如下:对于 L 中的每个 args,r := "length" args: f' args := (...(f (args_1))(args_2) ... (args_max(n,r))。这确定了 f' 唯一且定义明确。
  • 不错。 (虽然不是那么那么容易,是吗?)好吧,András Kovács 演示了如何在 Haskell 中使用 OverlappingInstances 或封闭类型族来表达这种独特的类型映射(这确实看起来很像你的数学定义)。但是,我坚持认为,您最好避免所有这些,只需不列出您的函数...
  • 正式版本在“非 ASCII 数学”中可能看起来更干净/更容易; ) - 我什至看不到一个合理的理由来列出我的功能;如果“listify”是标准库的一部分(意思是:安全且易于使用),在某些情况下,代码高尔夫可能是一个原因。
猜你喜欢
  • 2011-10-20
  • 2021-10-06
  • 1970-01-01
  • 2017-04-06
  • 2017-07-03
  • 1970-01-01
  • 2021-11-28
  • 1970-01-01
  • 2014-03-29
相关资源
最近更新 更多