是否可以部分应用 Haskell 中的第 n 个参数?

Is it possible to partially apply nth parameter in Haskell?

我很好奇是否可以编写一个函数 apply_nth 接受一个函数、参数的数量和该参数的值,然后 returns 一个新的、部分应用的函数.

我的感觉是,由于类型系统的原因,这是不可能的,但我想不出一个令人满意的答案。我也想不出一个有效的类型签名。

如果语言类型更松散,我想代码可能看起来像这样。

apply_nth f 0 x = f x
apply_nth f n x = \a -> apply_nth (f a) (n-1) x

有什么想法吗?

你的感觉是对的,这不可能。部分应用程序会更改函数的类型,并且以何种方式取决于您应用的参数。但是,如果该参数仅在运行时使用额外参数进行索引,编译器将不知道类型是什么,并且编译器必须对所有内容进行类型检查。确实,您需要结果具有 dependent type,但 Haskell 不是依赖类型的语言。

现在,实际上,如果你加入几个 GHC 扩展并引入几个奇怪的类型系列,那么你实际上可以实现类似于这种依赖类型的东西。但老实说,我怀疑这是个好主意。无论如何,你需要这个做什么?如果你正在使用超过 8 个参数的函数,你可能做错了什么,对于更简单的函数,你可以只定义 8 个组合器,每个组合器应用一个固定的参数位置。

或者:可能合理的类似功能是

apply_nth :: ([a] -> b) -> Int -> a -> [a] -> b
apply_nth f i a xs = f $ before ++ [a] ++ after
 where (before, after) = splitAt i xs

与参数列表不同,值列表的长度很容易达到数百个元素,因此在这种情况下,预先应用在运行时索引的单个元素是有意义的。


这不仅仅是一个安全预防措施——它是必要的,因为类型在运行时甚至不存在,所以编译器需要完成所有准备工作可能取决于类型的条件。这就是为什么 Haskell 安全 and 简洁 and 快速 and 可扩展语言。

当然,有点类型 class "magic":

{-# LANGUAGE DataKinds, KindSignatures, UndecidableInstances #-} 

data Nat = Z | S Nat 

data SNat (n :: Nat) where 
  SZ :: SNat Z 
  SS :: SNat n -> SNat (S n) 

class ApplyNth (n :: Nat) arg fn fn' | n arg fn -> fn', n fn -> arg where 
  applyNth :: SNat n -> arg -> fn -> fn' 

instance ApplyNth Z a (a -> b) b where 
  applyNth SZ a f = f a 

instance ApplyNth n arg' fn fn' => ApplyNth (S n) arg' (arg0 -> fn) (arg0 -> fn') where 
  applyNth (SS n) a f = \a0 -> applyNth n a (f a0)

applyNth 的一般类型表示,它需要一个索引(一个自然数 - 在类型中编码)、一个参数、一个函数和 returns 一个函数。

注意两个函数依赖关系。第一个说给定索引、参数和输入函数,输出函数的类型是已知的。这是显而易见的。第二个说,给定索引和输入函数,ApplyNth 能够查看函数内部并找出它需要什么参数!

此函数与类型推断配合得很好:

>:t \x -> applyNth (SS SZ) x (^)
\x -> applyNth (SS SZ) x (^)
  :: (Num fn', Integral b) => b -> fn' -> fn'
>:t applyNth (SS SZ) 0 (^)
applyNth (SS SZ) 0 (^) :: Num fn' => fn' -> fn'
>:t applyNth (SS SZ) (0 :: Integer) (^)
applyNth (SS SZ) (0 :: Integer) (^) :: Num fn' => fn' -> fn'
>:t applyNth (SS SZ) ('a' :: Char) (^)

<interactive>:1:32: Warning:
    Could not deduce (Integral Char) arising from a use of `^'
    ...
applyNth (SS SZ) ('a' :: Char) (^) :: Num fn' => fn' -> fn'
>let squared = applyNth (SS SZ) 2 (^)
>:t squared
squared :: Num fn' => fn' -> fn'
>squared 3
9
>squared 100
10000
>let f a b c d e = mapM_ putStrLn 
      [ show n ++ ": " ++ x 
      | (n,x) <- zip [0..]  
          [show a, show b, show c, show d, show e] ]
>applyNth SZ 'q' $ 
 applyNth (SS $ SZ) [1,8,42] $ 
 applyNth SZ (True, 10) $ 
 applyNth (SS $ SS $ SS SZ) "abcd" $ 
 applyNth (SS $ SS $ SS SZ) pi $
 f
0: (True,10)
1: 'q'
2: [1,8,42]
3: 3.141592653589793
4: "abcd"

你也可以定义为运算符形式:

infixl 9 =:
(=:) :: ApplyNth n arg fn fn' => SNat n -> arg -> fn -> fn' 
(=:) = applyNth

r = 
 SZ =: 'q' $ 
 SS SZ =: [1,8,42] $ 
 SZ =: (True, 10) $ 
 (SS $ SS $ SS SZ) =: "abcd" $ 
 (SS $ SS $ SS SZ) =: pi $ 
 f

不在任何称为 "Haskell" 的语言中,但如果您查看 Glasgow Haskell,包括不安全的函数,那么您可以按照您想要的方式部分应用...那么您必须正确指定参数位置。这是一个可怕的黑客攻击。不要这样做,除非你非常适应......好吧......不要这样做。

这段代码来自于我问过类似问题(Using Typeable to partially apply function at run-time (any time types match))。

import Unsafe.Coerce

testBuild :: String
testBuild = let f = buildFunc typedFunction ("argument 'b'", 42::Int) 1
            in f "Look I have applied " " and it seems to work."

typedFunction :: String -> (String,Int) -> String -> String
typedFunction = (\a b c -> a ++ show b ++ c)

buildFunc :: f -> x -> Int -> g
buildFunc f x 0 = unsafeCoerce f x
buildFunc f x i =
      let res = \y -> (buildFunc (unsafeCoerce f y) x (i-1))
      in unsafeCoerce res

并且输出:

*Main> testBuild
"Look I have applied (\"argument 'b'\",42) and it seems to work."

注意,如果我们错误地指定了参数索引 (1),那么程序可能会出现段错误。

不是那种奇怪的家庭,但也不是特别好:

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

import Data.Proxy

type family Fun as b where
    Fun '[]       b = b
    Fun (a ': as) b = a -> Fun as b

data SL as where
    Sn :: SL '[]
    Sc :: SL as -> SL (a ': as)

applyN :: Proxy c -> SL as -> Fun as (b -> c) -> b -> Fun as c
applyN p  Sn    f y = f y
applyN p (Sc s) f y = \x -> applyN p s (f x) y

main = print $ applyN Proxy (Sc (Sc Sn)) zipWith [1,2,3] (-) [6,5,4] -- [5,3,1]

我们也可以把Proxy c打包成SL:

data SL as c where
    Sn :: SL '[] c
    Sc :: SL as c -> SL (a ': as) c

applyN :: SL as c -> Fun as (b -> c) -> b -> Fun as c
applyN  Sn    f y = f y
applyN (Sc s) f y = \x -> applyN s (f x) y

main = print $ applyN (Sc (Sc Sn)) zipWith [1,2,3] (-) [6,5,4] -- [5,3,1]

或者您可以简单地定义几个组合器:

z = id
s r f y x = r (f x) y
applyN = id

main = print $ applyN (s (s z)) zipWith [1,2,3] (-) [6,5,4] -- [5,3,1]