Я понятия не имею, насколько полезным будет это приложение, но мне стало любопытно из-за этого ответа C++ на мой вопрос.
Итак, учитывая, скажем, троичную f
и двоичную g
, например.
f x y z = x + 10*y + 100*z
g x y = x + 10*y
как я могу получить функцию h
, чтобы выполнялось следующее?
h f g 1 2 3 4 5 == (321, 54)
Ясно могу определить
h f g = \x y z v w -> (f x y z, g v w)
Но мне было любопытно узнать, можно ли это сделать в бесточечном стиле для каждой арии m
и n
, используя некоторую существующую абстракцию.
Для этого конкретного примера (m == 3 && n == 2
) pointfree.io показывает, что результат становится нечитаемым:
h = flip . ((flip . ((flip . (((.) . (.) . (,)) .)) .)) .)
но мне все еще любопытно, существует ли что-то еще.
Ради интереса я попытался механически применить комбинаторную логику для вывода формулы, но пока только для конкретного случая m == 3 && n == 2
:
b = (.) -- bluebird (names from "To Mock a Mockingbird")
c = flip -- cardinal (names from "To Mock a Mockingbird")
p = (,)
f x y z = x + 10*y + 100*z
g x y = x + 10*y
{-
p(fxyz)(gvw) = ?xyzvw, with ? in terms of p, f, g and known birds
(p(fxyz))(gvw)
B(B(p(fxyz)))gvw
CBg(B(p(fxyz)))vw
B(CBg)B(p(fxyz))vw
B(B(CBg)B)p(fxyz)vw
B(B(B(CBg)B)p)(fxy)zvw
B(B(B(B(CBg)B)p))(fx)yzvw
B(B(B(B(B(CBg)B)p)))fxyzvw
B(B(B(B(B(CBB)(CB)g)p)))fxyzvw
B(B(B(BB(B(CBB)(CB))gp)))fxyzvw
B(B(BB(BB(B(CBB)(CB))g)p))fxyzvw
B(B(B(BB)(BB(B(CBB)(CB)))gp))fxyzvw
B(BB(B(BB)(BB(B(CBB)(CB)))g)p)fxyzvw
BB(BB(B(BB)(BB(B(CBB)(CB)))g))pfxyzvw
B(BB)(B(BB)(B(BB)(BB(B(CBB)(CB)))))gpfxyzvw
C(C(B(BB)(B(BB)(B(BB)(BB(B(CBB)(CB))))))p)fgxyzvw
-}
h = c (c (b (b b) (b (b b) (b (b b) (b b (b (c b b) (c b)))))) p)
h f g 1 2 3 4 5 == (321, 54) -- True
Да, @WillemVanOnsem, но я думаю, ты понимаешь, о чем я.
основная проблема заключается в том, что, например, в зависимости от экземпляра класса типов что-то может быть функцией или нет. Так что нельзя узнать, например, через синтаксис, сколько там параметров. Тот факт, что функция имеет только один параметр, — это не «деталь реализации», это достаточно фундаментально.
Возьмем, к примеру, printf
, которая в каком-то смысле является «вариативной» функцией, потому что через класс типов результатом может быть либо String
, IO String
, либо функция, которая сопоставляется с каким-то другим элементом: hackage.haskell.org/package/base -4.18.0.0/документы/…
Обычным «категориальным» подходом было бы использование uncurry
, чтобы преобразовать «n-арную» функцию в функцию, принимающую большой кортеж, а затем использовать что-то вроде h1 &&& h2
вместе с некоторыми curry
(и проекциями для отбрасывания ненужных входных данных). Примерно так можно определить семантику лямбда-исчисления простого типа в декартовой замкнутой категории.
@chi, но uncurry
не имеет только одного аргумента, так что у меня все еще будет проблема, сколько раз его применять, не так ли?
@Enlico Да, вам нужно использовать «правильное» карри, которое зависит от арности. Как объясняют приведенные ниже ответы, не может быть действительно независимого от арности решения.
Как указывает Виллем Ван Онсем в комментариях, проблема с вашим вопросом заключается в том, что технически каждая функция в Haskell принимает ровно один параметр. Я знаю, что вы имеете в виду, и я знаю, что вы хотите сделать, но нет причин, по которым (+)
не может быть просто функцией с одним аргументом, которая возвращает функцию, а не функцию с двумя аргументами. В самом деле, если мы определим экземпляр Num
для чего-то вроде Int -> Int
, то вполне возможно, что (+)
является функцией с тремя аргументами!
С другой стороны, только потому, что количество аргументов нельзя вывести, это не означает, что вы обречены. Если вы согласны дать GHC несколько подсказок, вы можете делать все, что хотите, на уровне типа. Рассмотрим следующее:
data Nat = Zero | Succ Nat
type One = Succ Zero
type Two = Succ One
type Three = Succ Two
type Four = Succ Three
class NFun m n f g where
type family FunT m n f g
h :: f -> g -> FunT m n f g
instance NFun m n x y => NFun (Succ m) n (a -> x) y where
type FunT (Succ m) n (a -> x) y = a -> FunT m n x y
h f g a = h @m @n (f a) g
instance NFun Zero n x y => NFun Zero (Succ n) x (a -> y) where
type FunT Zero (Succ n) x (a -> y) = a -> FunT Zero n x y
h f g a = h @Zero @n f (g a)
instance NFun Zero Zero x y where
type FunT Zero Zero x y = (x,y)
h = (,)
(Возможно, это можно сделать красивее, используя GHC TypeLits, но у меня всегда возникают небольшие проблемы с выводом индукции, поэтому я сам вычислял натуральные числа.)
Здесь я определил класс типов NFun
, который принимает два числовых типа, указывающих, сколько аргументов будет принимать функция. Первый экземпляр предоставляет аргументы первой функции, а второй экземпляр предоставляет аргументы второй функции. Последний экземпляр объединяет результаты.
Вы можете использовать его следующим образом:
f x y z = x + 10*y + 100*z
g x y = x + 10*y
h @Three @Two f g 1 2 3 4 5 == (321, 54)
Можете ли вы показать, что если мы определим экземпляр Num для чего-то вроде Int -> Int, тогда вполне возможно, что (+) является функцией с 3 аргументами! означает?
@Enlico: (+)
имеет тип Num a => a -> a -> a
. Если я каким-то образом определяю instance Num ((->) b b)
, то есть функция (+) :: (b -> b) -> (b -> b) -> (b -> b)
или (+) :: (b -> b) -> (b -> b) -> b -> b
, функция, которая таким образом принимает две функции в качестве параметра и возвращает функцию, которая, таким образом, принимает третий параметр.
Для конкретного случая могу определить instance Num (a -> Integer) where (+) = liftA2 (+); fromInteger = const
. Тогда 3 + 4
— это функция, которая принимает значение и производит 7
. Другими словами, есть случай, когда (+)
является функцией трех аргументов.
Если функция не является конкретным типом, арность функции (если не учитывать тот факт, что, строго говоря, каждая функция имеет арность, равную единице), может быть переменной. Действительно, хороший пример — printf::PrintfType r => String -> r.
Это кажется простой функцией. Но есть проблема: r
здесь может быть любой экземпляр PrintfType
, а у нас в качестве экземпляров:
instance PrintfType a ~ () => PrintfType (IO a) where
-- ...
instance IsChar c => PrintfType [c] where
-- ...
instance (PrintfArg a, PrintfType r) => PrintfType (a -> r) where
-- ...
Первые два не так интересны. Последний есть. Действительно, это означает, что он может возвращать другую функцию.
Это означает, что можно использовать printf
как:
printf "foo" :: String
printf "foo %i" 14 :: String
printf "foo %i %i" 14 25 :: String
Они реализовали своего рода вариативную функцию, в которой количество параметров зависит от того, как вы используете printf
.
Хотя я не совсем доволен printf
, поскольку он не очень «безопасен» по сравнению со многими другими функциями Haskell, он демонстрирует, как можно создавать функции с переменным числом переменных. Например, можно передать слишком много или слишком мало параметров, и %i
не гарантируется получение целочисленного или числового значения, поэтому есть лучшие способы форматирования строк.
Но независимо от того, количество параметров не зависит от самой функции, поэтому вы можете вывести это не с точки зрения функции, а с точки зрения того, как она используется. Поэтому, если только типы не являются конкретными в функции, если используются классы типов, уже не так просто определить арность функции и, следовательно, «слить» две функции вместе.
Что вы имеете в виду, говоря, что это не очень «безопасно» по сравнению со многими другими функциями Haskell? Что в этом небезопасного? Если долго отвечать, могу задать еще один специальный вопрос. Если у вас нет ссылки на существующую.
@Enlico: не сказано, что количество параметров в строке совпадает с количеством параметров или что типы совпадают, например, у printf "foo %s %s bar" "qux"
есть дополнительный %s
или printf "foo" 42
, или вы можете передать строку, отформатированную с помощью %i
.
в Haskell нет m-ичных функций: каждая функция принимает ровно один параметр.