0

I'm working on some functions that take a record and return a slightly modified record.

For example

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

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

The function modifyRecord takes two arguments, both of the same type.

currentR is the current state of the record

and

baseR is the base state of the record

(i.e. No functions applied, never changed)


Composing several functions of this type means I'll have to compose partial functions, make a list of them

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

and then I'd fold over currentState with function like foldl (flip ($))

so each fnn baseState is a function in itself with type SomeRecord -> SomeRecord


What I want to do is write those functions such that they only take the current state of the record and figure out base state on their own.

So

modifyRecord :: SomeRecord -> SomeRecord -> SomeRecord

to

modifyRecord :: SomeRecord -> SomeRecord

without actually modifying the record itself.

I want to avoid doing this

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

where the record itself would hold base values and function applied on it will avoid interacting with *Base items.

Would that be possible?

atis
  • 881
  • 5
  • 22

3 Answers3

2

Sounds like a job for the Reader monad.

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

Instead of passing baseR as an argument to each function explicitly, you access it as part of an environment.

Then you can write something like

runReader (foldl (>=>) return [fn1, fn2, ..., fnn] currentR) baseR
  1. foldl (>=>) return [fn1, fn2, ... fnn] reduces the list of Kleisli arrows to a single arrow, much like foldl (.) id composes a list of ordinary functions into a a single function.

  2. Applying the result of foldl to currentR produces a Reader SomeRecord SomeRecord value that only needs a base record to "kick off" the chain of modifications to the original current record and producing the final result.

    (Steps 1 and 2 generalize a fixed length chain like return currentR >>= fn1 >>= fn2 >>= fn3.)

  3. runReader supplies that base record by extracting the function from the Reader value and applying it to baseR.

Community
  • 1
  • 1
chepner
  • 497,756
  • 71
  • 530
  • 681
2

Put the initial state and current state in a tuple, and use fmap to lift functions that only care about the current state:

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

But what if we are given two functions in the form (SomeRecord,SomeRecord) -> SomeRecord and we need to compose them? We can define an operator easily enough, but does it exist somewhere already?

As it happens, the type ((,) e) has a Comonad instance. It is a very simple comonad that pairs values with some environment—in our case, the original value that we want to carry around.

The co-kleisli composition operator =>= can be used to chain two (SomeRecord,SomeRecord) -> SomeRecord functions, along with =>> to apply them to an initial paired value.

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

Or we can use =>> all the way:

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

Using the flipped fmap operator <&>, we can even write a pipeline like

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

We can also use extract to get the current value, which is perhaps better than snd for showing intent.

danidiaz
  • 26,936
  • 4
  • 45
  • 95
0

Broadly speaking, no, that is not possible: functions must explicitly declare all their inputs. Probably the cleanest way forward would be to use concatM to combine your functions. You will need to flip their arguments, so that the unmodified record comes last, not first; once you do, then you will have

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

as required. For combining just two such functions, there is

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

in base; f >=> g will perform the modifications of f first, then those of g. If you prefer the other order, to be closer to the behavior of (.), there is also a (<=<).

Daniel Wagner
  • 145,880
  • 9
  • 220
  • 380