Как понять каррирование и частичное применение функции? В чём их различия?

Как я понял, в хаскелле все функции > 1 аргумента становятся каррироваными. Как понять эту каррированность?

Допустим
add :: Int -> Int -> Int
add x y = x + y


При add 2 2 как я понял она переделается в add :: Int -> (Int -> Int)
И вот как это понять? Ну допустим функция add берёт первый аргумент 2 и возвращает функцию add которая ожидает второй аргумент 2? Что вообще происходит в теле возвращаемой функции? Неужели что -то вроде
add :: Int -> Int
add y = 2 + y

А если у функции аргументов намного больше?

А как вообще работает частичное применение? Что это такое?
  • Вопрос задан
  • 211 просмотров
Пригласить эксперта
Ответы на вопрос 3
Vindicar
@Vindicar
plyk, вот частичное применение в упрощённом варианте для двух аргументов.
def partial(func: typing.Callable[[int, int], int], arg1: int) -> typing.Callable[[int], int]:
    def wrapper(arg2: int) -> int:
        return func(arg1, arg2)
    return wrapper

def add (x: int, y: int) -> int:
    return x + y

def div(x: int, y:int) -> int:
    return x // y

add_42 = partial(add, 42)
print(add_42(3))  # 42 + 3 = 45

div_120 = partial(div, 120)
print(div_120(30))  # 120 // 30 = 4


А каррирование - по сути, каррированная функция вместо выбрасывания ошибки "недостаточно аргументов" возвращает частично применённую функцию для себя и указанных аргументов. Вот очень упрощённая реализация для функции двух аргументов:
def curry(func: typing.Callable[[int, int], int]) -> typing.Callable:
    def wrapper(*args):
        if len(args) == 2:
            return func(*args)
        elif len(args) == 1:
            return partial(func, args[0])
        else:
            raise Exception('Invalid arguments')
    return wrapper

curried_add = curry(add)
print(curried_add(32, 23))  # 55
add_30 = curried_add(30)
print(add_30(70))  # 30 + 70 = 100
Ответ написан
@AlexSku
Программист по автоматике
1) скобки в описании типа расставляются вправо, так что может быть любое количество аргументов:
fun:: a -> (a -> (a -> a))
2) частичное применение означает, что функции можно скармливать аргументы по одному в любом количестве.
(в режиме оператора можно подсунуть второй, а первый - потом).
3) про каррирование. Есть вид передачи параметров в других языках fun(p1, p2, p3). На Haskell это передача одной группы (кортежа). Есть функции высшего порядка curry и uncurry, которые вашу функцию переводят в противоположный тип.
Ответ написан
Комментировать
Наверное, вы имели ввиду add 2, тогда мы применим только первый аргумент функции. Когда мы так делаем, на выходе получаем функцию с одним аргументом, потому что первый аргумент уже равен 2. Можете считать, что он просто сохранен внутри этой функции, чтобы быть использованным, когда функция будет полностью применена.

Что вообще происходит в теле возвращаемой функции?

Так немного некорректно рассуждать, потому что компилятор может по-разному оптимизировать внутренность функции.

Но если вам будет проще понять, можно представить, что у нас не Хаскель, а обычный императивный язык. Тогда частичное применение можно эмулировать с помощью замыкания:
func main() {
	fmt.Println(sum(1, 2))

	carried := carry(sum, 1)
	fmt.Println(carried(2))
}

func sum(a, b int) int {
	return a + b
}

func carry(fn func(int, int) int, a int) func(int) int {
	return func(b int) int {
		return fn(a, b)
	}
}

https://go.dev/play/p/dp6W3U5bSGq

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

Войдите, чтобы написать ответ

Войти через центр авторизации
Похожие вопросы