Можно ли кодировать общую функцию "подъема" в Haskell?

Я не самый большой поклонник varargs, но я всегда считал, что стили аппликативного (f <$> x <*> y) и идиомы ([i| f x y |]) слишком много символов. Обычно я предпочитаю идти liftA2 f x y, но я тоже думаю, что A2 немного уродлив. Из этого вопроса, я узнал, что в Haskell можно реализовать функции vararg. Таким образом, можно ли использовать тот же принцип для реализации функции подъема, что:

lift f a b == pure f <*> a <*> b

Я попытался заменить + на <*> на цитируемый код:

class Lift r where 
    lift :: a -> r

instance Lift a where
    lift = id

instance (Lift r) => Lift (a -> r) where
    lift x y = lift (x <*> y)

Но мне не удалось получить правильные типы...

Ответ 1

Обратите внимание, что вы можете связать любое число <*>, чтобы получить функцию формы

f (a0 -> .. -> an) -> (f a0 -> .. -> f an)

Если у нас есть тип a0 -> .. -> an и f a0 -> .. -> f an, мы можем вычислить f из этого. Мы можем кодировать это отношение и наиболее общий тип, как следует

class Lift a f b | a b -> f where 
  lift' :: f a -> b 

Как вы и ожидаете, экземпляр "рекурсивный случай" просто применит <*> один раз, а затем рекурсирует:

instance (a ~ a', f' ~ f, Lift as f rs, Applicative f) 
      => Lift (a -> as) f (f' a' -> rs) where  
  lift' f a = lift' $ f <*> a

Основной случай - когда функции больше нет. Поскольку вы не можете утверждать, что "a не является типом функции", это зависит от совпадающих экземпляров:

instance (f a ~ b) => Lift a f b where 
  lift' = id 

Из-за правил выбора экземпляров GHCs рекурсивный случай всегда выбирается, если это возможно.

Тогда вам нужна функция lift' . pure:

lift :: (Lift a f b, Applicative f) => a -> b
lift x = lift' (pure x) 

Здесь важна функциональная зависимость от Lift. Так как f упоминается только в контексте, эта функция будет плохо напечатана, если мы не сможем определить, что f знает только a и b (которые появляются в правой части =>),

Для этого требуется несколько расширений:

{-# LANGUAGE 
    OverlappingInstances
  , MultiParamTypeClasses
  , UndecidableInstances
  , FunctionalDependencies
  , ScopedTypeVariables
  , TypeFamilies
  , FlexibleInstances
  #-}

и, как обычно, с вариационными функциями в Haskell, обычно единственный способ выбрать экземпляр - дать явную подпись типа.

lift (\x y z -> x * y + z) readLn readLn readLn :: IO Int

Как я его написал, GHC с радостью примет Lift, который является полиморфным в аргументах f (но не f).

lift (+) [1..5] [3..5] :: (Enum a, Num a) => [a]

Иногда контекст достаточно, чтобы вывести правильный тип. Обратите внимание, что тип аргумента снова является полиморфным.

main = lift (\x y z -> x * y + z) readLn readLn readLn >>= print 

Как по GHC >= 7.10, OverlappingInstances устарел, и компилятор выдаст предупреждение. Вероятно, он будет удален в более поздней версии. Это можно устранить, удалив OverlappingInstances из прагмы {-# LANGUAGE .. #-} и изменив второй экземпляр на

instance {-# OVERLAPS #-} (f a ~ b) => Lift a f b where 

Ответ 2

Я предполагаю, что вы предпочитаете использовать lift без аннотаций типа. В этом случае есть в основном два варианта:

Сначала, если мы используем OverlappingInstances, полиморфным функциям нужны аннотации:

{-# LANGUAGE
  OverlappingInstances,
  MultiParamTypeClasses,
  UndecidableInstances,
  FunctionalDependencies,
  FlexibleInstances,
  TypeFamilies
  #-}

import Control.Applicative

class Applicative f => ApN f a b | a b -> f where
  apN :: f a -> b

instance (Applicative f, b ~ f a) => ApN f a b where
  apN = id

instance (Applicative f, ApN f a' b', b ~ (f a -> b')) => ApN f (a -> a') b where
  apN f fa = apN (f <*> fa)

lift :: ApN f a b => a -> b
lift a = apN (pure a)

-- Now we can't write "lift (+) (Just 0) Nothing"
-- We must annotate as follows: 
--   lift ((+) :: Int -> Int -> Int) (Just 0) Nothing
-- Monomorphic functions work fine though:
--   lift (||) (Just True) (Just True) --> results in "Just True"

Второй, если вместо этого использовать IncoherentInstances, lift будет работать без аннотаций даже по полиморфным функциям. Однако некоторые сложные вещи все равно не будут проверяться, например (lift . lift) (+) (Just (Just 0)) Nothing.

{-# LANGUAGE 
  IncoherentInstances, MultiParamTypeClasses,
  UndecidableInstances,ScopedTypeVariables,
  AllowAmbiguousTypes, FlexibleInstances, TypeFamilies
  #-}

import Control.Applicative

class Applicative f => ApN f a b where
  apN :: f a -> b

instance (Applicative f, b ~ f a) => ApN f a b where
  apN = id

instance (Applicative f, ApN f a' b', b ~ (f a -> b')) => ApN f (a -> a') b where
  apN f fa = apN (f <*> fa)

lift :: forall f a b. ApN f a b => a -> b
lift a = (apN :: f a -> b) (pure a)

-- now "lift (+) (Just 0) (Just 10)" works out of the box

Я представил два решения вместо одного с IncoherentInstances, потому что IncoherentInstances является довольно грубым расширением, которого следует избегать, если это возможно. Вполне возможно, что здесь хорошо, но я счел целесообразным предоставить альтернативное решение. В любом случае.


В обоих случаях я использую один и тот же трюк, чтобы помочь вывести и уменьшить аннотации: я пытаюсь переместить информацию из голов экземпляра в ограничения экземпляра. Поэтому вместо

instance (Applicative f) => ApN f a (f a) where
  apN = id

Я пишу

instance (Applicative f, b ~ f a) => ApN f a b where
  apN = id

Кроме того, в другом случае я использую простой параметр b в голове экземпляра и добавляю b ~ (f a ~ b') к ограничениям.

Причиной этого является то, что GHC сначала проверяет, есть ли соответствующая глава экземпляра, и пытается устранить ограничения только после успешного совпадения. Мы хотим наложить наименьшую нагрузку на голову экземпляра, и пусть решатель ограничений сортирует вещи (потому что он более гибкий, может задерживать суждения и может использовать ограничения из других частей программы).