Можно ли частично применить n-й параметр в Haskell?

Мне любопытно, можно ли написать функцию apply_nth, которая принимает функцию, номер параметра и значение этого параметра, а затем возвращает новую, частично примененную функцию.

У меня такое ощущение, что это невозможно из-за системы типов, но я не могу придумать удовлетворительного ответа. Я также не могу придумать подпись рабочего типа.

Я думаю, что если бы язык был более свободно типизирован, код мог бы выглядеть так.

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

Любые идеи?


person danmcardle    schedule 22.10.2015    source источник
comment
Поиск функций с переменным числом аргументов в Haskell   -  person Bergi    schedule 23.10.2015
comment
В частности, это   -  person Bartek Banachewicz    schedule 23.10.2015
comment
Я полагаю, что давным-давно у меня было что-то сказать об этой проблеме. Некоторые, возможно, все еще отрицают это, но Haskell на практике довольно долгое время типизировался довольно зависимо.   -  person pigworker    schedule 26.10.2015


Ответы (4)


Ваше ощущение правильное, это невозможно. Частичное применение изменяет тип функции, и каким образом это зависит от того, какой параметр вы применяете. Но если этот параметр индексируется только во время выполнения с дополнительным аргументом, компилятор не знает, какой будет тип, и компилятор должен проверять тип всего. На самом деле вам нужно, чтобы результат имел зависимый тип, но 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 безопасен, и краток, и быстр, и расширяем, как некоторые другие языки.

person leftaroundabout    schedule 22.10.2015
comment
Это не часть более крупного проекта или что-то в этом роде. Я думал о лямбда-исчислении и обработке функций как данных, и мне было интересно, можно ли это сделать. Спасибо за ваш ответ! - person danmcardle; 23.10.2015

Не такие странные семьи, но и не очень приятные:

{-# 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]
person user3237465    schedule 22.10.2015

Конечно, с небольшим количеством типа «магия»:

{-# 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 говорит, что он принимает индекс (натуральное число, закодированное в типе), аргумент, функцию и возвращает функцию.

Обратите внимание на две функциональные зависимости. Первый говорит, что для данного индекса, аргумента и входной функции известен тип выходной функции. Это многое очевидно. Второй говорит, что с учетом индекса и входной функции 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
person user2407038    schedule 22.10.2015

Не внутри какого-либо языка под названием «Haskell», но если вы посмотрите на Glasgow Haskell, включая небезопасные функции, то вы можете частично применить его так, как хотите... ну, вам нужно правильно указать местоположение аргумента. ЭТО УЖАСНЫЙ ВЗЛОМ. Не делайте этого, если вы не чувствуете себя очень комфортно с... ну... не делайте этого.

Этот код вернулся, когда я задал аналогичный вопрос (Использование Typeable для частичного применения функции во время выполнения (любое совпадение типов времени)).

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), то программа, скорее всего, выдаст ошибку сегментации.

person Thomas M. DuBuisson    schedule 22.10.2015
comment
Как он говорит: Не делай этого! - person augustss; 23.10.2015
comment
Да, безусловно, стоит повторить. - person Thomas M. DuBuisson; 23.10.2015