Создание числовых функций - экземпляр Num?

Я хочу иметь возможность составлять числовые функции в haskell, используя двоичные операторы. Так, например, с унарными числовыми функциями:

f*g

следует перевести на:

\x -> (f x)*(g x)

и аналогично для сложения. Создание собственного оператора для этого довольно просто, но мне бы очень хотелось просто сделать Num a => a -> a функции экземпляром Num, но я не уверен, как это сделать.

Я бы также хотел, чтобы эта ария была родовой, но это может быть слишком большой проблемой для того, насколько сложно выполнять общие функции arity в Haskell, поэтому было бы лучше определить отдельный Num a => a -> a -> a, Num a => a -> a -> a -> a, и т.д. экземпляры до некоторого достаточно большого количества.

Ответ 1

экземпляр с общей арностью

instance Num b => Num (a->b) where
    f + g = \x -> f x + g x
    f - g = \x -> f x - g x
    f * g = \x -> f x * g x
    negate f = negate . f
    abs f = abs . f
    signum f = signum . f
    fromInteger n = \x -> fromInteger n

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

Ответ 2

Идиоматический подход

Существует экземпляр Applicative для (->) a, что означает, что все функции являются аппликативными функторами. Современная идиома для составления любых функций в том, как вы описываете, - это использовать Applicative, например:

(*) <$> f <*> g
liftA2 (*) f g -- these two are equivalent

Это делает операцию чистой. Оба эти подхода несколько более подробные, но мне представляется более четко выраженным комбинационный рисунок.

Кроме того, это гораздо более общий подход. Если вы понимаете эту идиому, вы сможете применить ее во многих других контекстах, чем просто Num. Если вы не знакомы с Applicative, одно место для запуска - Typeclassopedia. Если вы теоретически настроены, вы можете проверить McBride и знаменитую статью Patterson. (Для записи я использую "идиому" здесь в обычном смысле, но помню о каламбуре.)

Num b => Num (a -> b)

Экземпляр, который вы хотите (и другие, кроме того), доступен в пакет NumInstances. Вы можете скопировать экземпляр @genisage; они функционально идентичны. (@genisage более подробно изложил это, сравнение двух реализаций может быть просвещенным.) Импорт библиотеки в Hackage имеет преимущество выделения для других разработчиков того, что вы используете экземпляр-сирота.

Однако проблема с Num b => Num (a -> b). Короче говоря, 2 теперь не только число, но и функция с бесконечным числом аргументов, все из которых она игнорирует. 2 (3 + 4) теперь равно 2. Любое использование целочисленного литерала как функции почти наверняка даст неожиданный и неправильный результат, и нет способа предупредить программиста, что он не является исключением.

Как указано в Раздел отчета Haskell 2010 6.4.1, "Целый литерал представляет приложение функции fromInteger для соответствующее значение типа Integer." Это означает, что запись 2 или 12345 в исходном коде или в GHCi эквивалентна записи fromInteger 2 или fromInteger 12345. Поэтому любое выражение имеет тип Num a => a.

В результате, fromInteger абсолютно повсеместно распространен в Haskell. Обычно это получается чудесно; когда вы пишете номер в исходном коде, вы получаете число - соответствующего типа. Но с вашим экземпляром Num для функций тип fromInteger 2 вполне может быть a -> Integer или a -> b -> Integer. На самом деле GHC с радостью заменит литерал 2 функцией, а не числом, и особенно опасной функцией, которая отбрасывает любые данные, данные ей. (fromInteger n = \_ -> n или const n, т.е. выкинуть все аргументы и просто дать n.)

Часто вы можете уйти, не внедряя неприменимые члены класса, или реализуя их с помощью undefined, либо из которых возникает ошибка времени выполнения. Это по тем же причинам не является проблемой для данной проблемы.

Более разумный пример: pseudo- Num a => Num (a -> a)

Если вы хотите ограничить себя умножением и добавлением унарных функций типа Num a => a -> a, мы можем немного облегчить задачу fromInteger или, по крайней мере, 2 (3 + 5) равным 16, а не 2, Ответ состоит в том, чтобы просто определить fromInteger 3 как (*) 3, а не const 3:

instance (a ~ b, Num a) => Num (a -> b) where
  fromInteger = (*) . fromInteger
  negate      = fmap negate
  (+)         = liftA2 (+)
  (*)         = liftA2 (*)
  abs         = fmap abs
  signum      = fmap signum

 

ghci> 2 (3 + 4)
14
ghci> let x = 2 ((2 *) + (3 *))
ghci> :t x
x :: Num a => a -> a
ghci> x 1
10
ghci> x 2
40

Обратите внимание, что хотя это, по-видимому, морально эквивалентно Num a => Num (a -> a), оно должно быть определено с помощью ограничения равенства (для которого требуется GADTs или TypeFamilies). В противном случае мы получим ошибку двусмысленности для чего-то вроде (2 3) :: Int. Я не собираюсь объяснять, почему, извините. В принципе, ограничение равенства a ~ b => a -> b позволяет предполагаемому или заявленному типу b распространяться на a во время вывода.

Для приятного объяснения того, почему и как работает этот экземпляр, см. ответ на Числа как мультипликативные функции (странные, но интересные).

Предупреждение о сиротских экземплярах

Не выставляйте какой-либо модуль, который содержит - или импортирует модуль, который содержит, или импортирует модуль, который импортирует модуль, содержащий... - любой из этих экземпляров, не понимая проблему сиротские экземпляры и/или предупреждать своих пользователей соответственно.