6

我想编写一个 Haskell 函数,它的作用类似于翻转,但更通用,可以使函数的任何参数成为最后一个参数。为方便起见,我们用pull它来表示。

编写以下代码很容易:

Prelude> :t flip          --we just call this function a swap
flip :: (a -> b -> c) -> b -> a -> c
Prelude> :t (flip.)       --we just call this function a swap
(flip.) :: (a -> a1 -> b -> c) -> a -> b -> a1 -> c
Prelude> :t ((flip.).)    --we just call this function a swap
((flip.).) :: (a -> a1 -> a2 -> b -> c) -> a -> a1 -> b -> a2 -> c
Prelude> :t (((flip.).).) --we just call this function a swap
(((flip.).).)
  :: (a -> a1 -> a2 -> a3 -> b -> c) -> a -> a1 -> a2 -> b -> a3 -> c

我们发现,将 more (.) 应用于翻转,它可以交换任意相邻的参数对。有了上面的结果,我们可以写:

Prelude> :t flip
flip :: (a -> b -> c) -> b -> a -> c
Prelude> :t (flip.) . flip
(flip.) . flip :: (a1 -> a -> b -> c) -> a -> b -> a1 -> c
Prelude> :t ((flip.).) . (flip.) . flip
((flip.).) . (flip.) . flip
  :: (a2 -> a -> a1 -> b -> c) -> a -> a1 -> b -> a2 -> c
Prelude> :t (((flip.).).) . ((flip.).) . (flip.) . flip
(((flip.).).) . ((flip.).) . (flip.) . flip
  :: (a3 -> a -> a1 -> a2 -> b -> c) -> a -> a1 -> a2 -> b -> a3 -> c

我们可以发现,随着更多的交换组合,它可以将任意参数拉到最后一个位置。所以我们可以在很多情况下摆脱 lambda 表达式。但是上面的快递很臃肿。

我的主要想法是制作一个函数pull来概括上述功能。pull行为大致如下。

let f = undefined               --For convenience, we let f be undefined.

:t pull 0 (f::a->b->z)          --the type variable z is not a function type.
>pull 0 (f::a->b->z) :: b->a->z --pull is just like flip for 0 and a function of this type.
:t pull 0 (f::a->b->c->z)       --the type variable z is not a function type.
>pull 0 (f::a->b->c->z) :: b->c->a->z

:t pull 1 (f::a->b->c->z)       --the type variable z is not a function type.
>pull 1 (f::a->b->c->z) :: a->c->b->z
:t pull 1 (f::a->b->c->d->z)    --the type variable z is not a function type.
>pull 1 (f::a->b->c->d->z) :: a->c->d->b->z

:t pull 2 (f::a->b->c->d->z)    --the type variable z is not a function type.
>pull 2 (f::a->b->c->d->z) :: a->b->d->c->z
:t pull 2 (f::a->b->c->d->e->z) --the type variable z is not a function type.
>pull 2 (f::a->b->c->d->e->z) :: a->b->d->e->c->z

我尝试了很多方法来做到这一点。最天真的是:

swap :: Word -> a -> a
swap 0 = flip
swap n = dot $ swap (n-1)

和 ghc 抱怨如下,我明白为什么:

-- Prelude> :reload
-- [1 of 1] Compiling Main             ( ModifyArbitrayParameterOfAFunction.hs, interpreted )
--
-- ModifyArbitrayParameterOfAFunction.hs:4:21:
--     Occurs check: cannot construct the infinite type: c0 = a1 -> c0
--     Expected type: (a1 -> c0) -> c0
--       Actual type: (a1 -> c0) -> a1 -> c0
--     In the return type of a call of `modify'
--     Probable cause: `modify' is applied to too few arguments
--     In the first argument of `(.)', namely `(modify (n - 1) modi)'
--     In the expression: (modify (n - 1) modi) . f1
--
-- ModifyArbitrayParameterOfAFunction.hs:4:42:
--     Occurs check: cannot construct the infinite type: c0 = a1 -> c0
--     Expected type: a1 -> a1 -> c0
--       Actual type: a1 -> c0
--     In the second argument of `(.)', namely `f1'
--     In the expression: (modify (n - 1) modi) . f1
--     In an equation for `modify':
--         modify n modi f1 = (modify (n - 1) modi) . f1
-- Failed, modules loaded: none.

也许我的目标只是一厢情愿,但考虑到 Haskell 的类型系统甚至可以编写 lambda 表达式,我敢说一定有办法做到这一点。

4

3 回答 3

4

您不能将其作为普通函数执行,因为函数的类型会因输入而异。您可以使用类型类和功能依赖项引入的临时多态性来做到这一点。但是,即便如此,您仍需要大量扩展来允许像 Oleg 那样的东西IsFunction(参见: http: //okmij.org/ftp/Haskell/isFunction.lhs)。那是缺少的部分,可以让您确定是否已达到类型类递归的基本情况。

于 2013-06-17T14:38:30.310 回答
1

正如评论中提到的,你不能有一个函数来传递你想要翻转的函数的数量。参数是在运行时计算的,而您在编译时需要该值,以便您可以确定正确的类型。

如果不以某种方式传递它,你也无法做到。例如a -> b -> c -> d,可以将其视为一个返回三个参数的函数d,或者将其视为一个返回两个参数的函数c -> d

可能最简单的解决方案是明确定义函数,例如flip2flip3。我知道这不是您要寻找的,但它是最实用的解决方案。

另一种选择是使用 Template Haskell。然后,情况就不同了,因为 Template Haskell 在编译时执行(我会说“元”)代码。使用 TH,您可以创建一个函数,该函数采用自然数并生成可以编译到另一个模块中的 TH 表达式。元函数可以定义为

{-# LANGUAGE TemplateHaskell #-}
module GenFlipTH where

import Language.Haskell.TH

pull :: Int -> Q Exp
pull 0              = varE 'flip
pull n | n < 0      = fail "Negative arity"
       | otherwise  = [| fmap $(pull (n - 1)) . flip |]
-- Here we use the fact that `(->) r` is a functor.

并在另一个模块中使用以生成适当的表达式,例如

{-# LANGUAGE TemplateHaskell #-}
import GenFlipTH

flip3 :: (a -> b3 -> b2 -> b1 -> b -> c) -> (b3 -> b2 -> b1 -> b -> a -> c)
flip3 = $( pull 3 )

这可能接近我可以获得的您的要求 - 您通过数字确定函数并获得编译时保证它被正确创建和使用。

于 2013-06-17T17:41:29.317 回答
0

如果我们没有类型系统施加的任何限制,我会使用以下函数,它将位置 n 处的参数移动到位置 m。

moveArg n m f =
  if n == 1 && m == 1 then f
  else if n > 1 && m > 1 then
    \x -> moveArg (n-1) (m-1) (f x)
  else if n == 1 then   -- Here m > 1
    moveArg 2 m (\x y -> f y x)
  else -- m == 1 && n > 1
    \x y -> (moveArg n 2 f) y x

您可以用非类型化语言(例如 Javascript、Python)轻松实现此功能。注意:

flip == moveArg 1 2 == moveArg 2 1
moveArg 1 3 == \f a b c -> f b c a
moveArg 2 4 == \f a b c d -> f a c d b
etc.

但是,我找不到可以键入此类函数的语言。为此,我们需要一种方法来以编程方式创建一个依赖类型签名,f它依赖于nm作为常量,例如:

moveArg: (n: Int) -> (m: Int) -> mkArgSubstitution n m

编写一个计算类型的程序并不难mkArgSubstitution,例如:

mkArgSubstitution 1 2 = forall a b c. (a -> b -> c) -> (b -> a -> c)
mkArgSubstitution 2 4 = forall a b c d e. (a -> b -> c -> d -> e) -> (a -> c -> d -> b -> e)
...
于 2018-06-20T15:52:51.560 回答