Как Хаскелл "сочиняет линзы с использованием функционального состава" с этим странным порядком аргументов может быть реализован?

Я читал учебник wreq:

Объектив обеспечивает возможность сосредоточиться на части значения Haskell. Для Например, тип Response имеет объектив responseStatus, который фокусируется на информации о состоянии, возвращаемой сервером.

ghci> r ^. responseStatus
Status {statusCode = 200, statusMessage = "OK"}

Оператор ^. принимает в качестве первого аргумента значение, а объектив - как во-вторых, и возвращает часть значения, сфокусированного объективом.

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

ghci> r ^. responseStatus . statusCode
200

Я не могу придумать, как состав функций, выполненный с помощью этого порядка аргументов, может обрабатывать структуру вложенности в этом порядке.

Посмотрите: r ^. responseStatus . statusCode может быть либо r ^. (responseStatus . statusCode), либо (r ^. responseStatus) . statusCode.

В первом говорится, что мы строим функцию, которая первая рассматривает statusCode (получает ее из записи Status?), как я могу вывести из показанного значения Status {statusCode = 200, statusMessage = "OK"}), а затем передает его на responseStatus, который должен обрабатывать статус ответа. Итак, это наоборот: на самом деле код статуса является частью статуса ответа.

Второе чтение также не имеет смысла для меня, потому что оно сначала обрабатывает код состояния.

Ответ 1

Правильное показание r ^. responseStatus . statusCode равно r ^. (responseStatus . statusCode). Это естественно, поскольку композиция функции возвращает функцию при применении к двум аргументам, поэтому (r ^. responseStatus) . statusCode должна возвращать функцию, а не любое значение, которое может быть распечатано.

Это все еще оставляет открытым вопрос, почему линзы формируются в "неправильном" порядке. Поскольку реализация объективов немного волшебна, рассмотрим более простой пример.

first - функция, которая отображает первый элемент пары:

first :: (a -> b) -> (a, c) -> (b, c)
first f (a, b) = (f a, b)

Что делает map . first? first принимает функцию, действующую на первый элемент, и возвращает функцию, действующую на пару, что более очевидно, если мы скопируем тип таким образом:

first :: (a -> b) -> ((a, c) -> (b, c))

Также вспомните тип map:

map :: (a -> b) -> ([a] -> [b])

map принимает функцию, действующую на элемент, и возвращает функцию, действующую на список. Теперь f . g работает, сначала применяя g, а затем подавая результат на f. Итак, map . first принимает функцию, действующую на некоторый тип элемента, преобразует ее в функцию, действующую на пары, а затем преобразует ее в функцию, действующую на списки пар.

(map . first) :: (a -> b) -> [(a, c)] -> [(b, c)]

first и map обе функции превращения, действующие на часть структуры, на функции, действующие на всю структуру. В map . first какова вся структура для first становится фокусом для map.

(map . first) (+10) [(0, 2), (3, 4)] == [(10, 2), (13, 4)]

Теперь взгляните на тип объективов:

type Lens = forall f. Functor f => (a -> f b) -> (s -> f t)

Попробуйте проигнорировать бит Functor. Если мы слегка прищурились, это напоминает типы для map и first. И так бывает, что линзы также преобразуют функции, действующие на части структур, в функцию, действующую на целые структуры. В сигнатуре выше s обозначает всю структуру, а a обозначает ее часть. Поскольку наша входная функция может изменить тип a на b (как указано a -> f b), нам также нужен параметр t, который примерно означает "тип s после того, как мы изменили a к b внутри него".

statusCode является линзой, которая преобразует функцию, действующую на a Int к функции, действующей на a Status:

statusCode :: Functor f => (Int -> f Int) -> (Status -> f Status)

responseStatus преобразует функцию, действующую на a Status к функции, действующей на a Response:

responseStatus :: Functor f => (Status -> f Status) -> (Response -> f Response)

Тип responseStatus . statusCode следует тому же шаблону, который мы видели с помощью map . first:

responseStatus . statusCode :: Functor f => (Int -> f Int) -> (Response -> f Response)

Пока не видно, как работает ^.. Он тесно связан с основным механиком и магией линз; Я не буду повторять его здесь, поскольку об этом написано немало работ. Для введения я рекомендую посмотреть этот и этот, и вы также можете смотреть это отличное видео.