Haskell - лучший способ отслеживать (начальное) состояние записи - PullRequest
1 голос
/ 14 марта 2019

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

Например,

import Control.Lens ((%~), (^.), (&))

modifyRecord :: SomeRecord -> SomeRecord -> SomeRecord
modifyRecord baseR currentR = currentR & thisPart %~ (fmap (someFn someValue))
        where someValue = baseR ^. thisPart

Функция modifyRecord принимает два аргумента, оба изтого же типа.

currentR - текущее состояние записи

, а

baseR - базовое состояние записи

(т.е.Функции не применены, никогда не менялись)


Составление нескольких функций этого типа означает, что мне придется составлять частичные функции, составлять их список

[fn1 baseState , fn2 baseState , fn3 baseState ... fnn baseState]

и затем я 'Сверните currentState с помощью функции, подобной foldl (flip ($))

, поэтому каждый fnn baseState сам по себе является функцией с типом SomeRecord -> SomeRecord


Что я хочу сделать, это написать этитакие функции, что они принимают только текущее состояние записи и самостоятельно определяют базовое состояние.

Итак

modifyRecord :: SomeRecord -> SomeRecord -> SomeRecord

до

modifyRecord :: SomeRecord -> SomeRecord

без фактической модификациисама запись.

Я хочу избежать этого

data SomeRecord = SomeRecord { value1 :: Float
                             , value1Base :: Float
                             , value2 :: Float
                             , value2Base :: Float
                             ...
                             ...
                             , valueN :: Float
                             , valueNBase :: Float
                             }

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

Возможно ли это?

Ответы [ 3 ]

3 голосов
/ 14 марта 2019

Похоже на задание для монады Reader.

modifyRecord :: SomeRecord -> Reader SomeRecord SomeRecord
modifyRecord currentR = do
     baseR <- ask
     currentR & thisPart %~ (fmap (someFn someValue))
        where someValue = baseR ^. thisPart

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

Затем вы можете написать что-то вроде

runReader (foldl (>=>) return [fn1, fn2, ..., fnn] currentR) baseR
  1. foldl (>=>) return [fn1, fn2, ... fnn] уменьшает список стрелок Клейсли до одной стрелки, так же как foldl (.) id составляет список обычных функций в видеодиночная функция.

  2. Применение результата foldl к currentR дает значение Reader SomeRecord SomeRecord, для которого нужна только базовая запись, чтобы "запустить" цепочку изменений в оригинале.текущая запись и получение окончательного результата.

    (Шаги 1 и 2 обобщают цепочку фиксированной длины, например return currentR >>= fn1 >>= fn2 >>= fn3.)

  3. runReader обеспечивает эту базовую записьизвлечение функции из значения Reader и ее применение к baseR.

2 голосов
/ 14 марта 2019

Поместите начальное и текущее состояние в кортеж и используйте fmap, чтобы поднять функции, которые заботятся только о текущем состоянии:

ghci> :set -XTypeApplications
ghci> fmap @((,) SomeRecord) :: (a -> b) -> (SomeRecord, a) -> (SomeRecord, b)

Но что, если нам даны две функции в форме (SomeRecord,SomeRecord) -> SomeRecord, и нам нужно их составить? Мы можем достаточно легко определить оператор, но существует ли он где-то уже?

Как это бывает, тип ((,) e) имеет экземпляр Comonad. Это очень простая comonad, которая связывает значения с некоторой средой - в нашем случае, с исходным значением, которое мы хотим сохранить.

Оператор композиции co-kleisli =>= может использоваться для объединения двух функций (SomeRecord,SomeRecord) -> SomeRecord вместе с =>> для применения их к начальному парному значению.

 ghci> import Control.Comonad
 ghci> (1,7) =>> ((\(o,c) -> c * 2) =>= (\(o,c) -> o + c))
 (1,15)

Или мы можем использовать =>> полностью:

 ghci> (1,7) =>> (\(o,c) -> c * 2) =>> (\(o,c) -> o + c)
 (1,15)

Используя оператор flipped fmap <&>, мы даже можем написать конвейер, например

 ghci> (1,2) =>> (\(x,y) -> y+2) <&> succ =>> (\(x,y) -> x + y)
 (1,6)  

Мы также можем использовать extract, чтобы получить текущее значение, которое, возможно, лучше, чем snd для показа намерения.

0 голосов
/ 14 марта 2019

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

concatM [f1, f2, f3, f4] :: SomeRecord -> SomeRecord -> SomeRecord

, как требуется.Для объединения только двух таких функций существует

(>=>) ::
    (SomeRecord -> SomeRecord -> SomeRecord) ->
    (SomeRecord -> SomeRecord -> SomeRecord) ->
    (SomeRecord -> SomeRecord -> SomeRecord)

в base;f >=> g сначала выполнит модификации f, а затем g.Если вы предпочитаете другой порядок, чтобы быть ближе к поведению (.), есть также (<=<).

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...