Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Are there any useful abstractions for Haskell's record syntax?

To try and simplify this problem I have defined these arrow functions:

splitA :: (Arrow arr) => arr a b -> arr a (b,a)
splitA ar = ar &&& (arr (\a -> id a))

recordArrow
   :: (Arrow arr)
   => (d -> r)
   -> (d -> r -> d)
   -> (r -> r)
   -> arr d d
recordArrow g s f = splitA (arr g >>^ f) >>^ \(r,d) -> s d r

Which then let's me do something like this:

unarrow :: ((->) b c) -> (b -> c) -- unneeded as pointed out to me in the comments
unarrow g = g


data Testdata = Testdata { record1::Int,record2::Int,record3::Int }

testRecord = unarrow $
       recordArrow record1 (\d r -> d { record1 = r }) id
   >>> recordArrow record2 (\d r -> d { record2 = r }) id
   >>> recordArrow record3 (\d r -> d { record3 = r }) id

As you can see this doesn't make very good use of DRY.

I'm hoping there might be some sort of language extension that could help simplify this process. So that the above could simply be re-written as:

testRecord' = unarrow $
       recordArrow' record1 id
   >>> recordArrow' record2 id
   >>> recordArrow' record3 id

Updated for clarity: To clarify a little bit. I'm aware that I could do something like this:

foo d = d { record1 = id (record1 d), record2 = id (record2 d) }

But this ignores any execution order and any state. Suppose the update function for record2 relies on the updated value for record1. Or alternatively, I may want to create a different arrow that looks like this: arr d (d,x) and then I want to build a list of [x] the order of which depends on the evaluation order of the records.

What I've found is that I frequently want to execute some functionality and afterwards update a record. I can do that by threading the state like this

g :: d -> r -> d
foo d = let d' = d { record1 = (g d) (record1 d) } in d' { record2 = (g d') (record2 d') }

But I think arrow notation is neater, and I could also have [arr d d] and chain them together in a sequence. Plus if r or d are Monads it creates neater code. Or if they're both Monads, it let's me perform layered binds without having to use a Monad Transformer. In the case of ST s x it let's me thread the state s around in an ordered way.

I'm not trying to solve one particular problem. I'm just trying to find an abstracted method of updating a record syntax without having to explicitly define some sort of "getter" and "setter".


Below was answered in the comments-- Sidenote: I've had to define a function unarrow for converting a function (->) arrow back to a function. Otherwise if I have someArrow b for an arrow arr b c I can't get to value c. With the unarrow function I can write unarrow someArrow b and it works fine. I feel like I must be doing something wrong here because my definition for unarrow is simply unarrow g = g.

like image 503
TheCriticalImperitive Avatar asked Sep 21 '14 00:09

TheCriticalImperitive


1 Answers

The abstraction you are looking for is called a lens and the lens package on Hackage is probably the most widespread implementation of the idea currently in use. Using the lens package you can define your recordArrow' as

{-# LANGUAGE RankNTypes #-}

import Control.Arrow
import Control.Lens

recordArrow' :: Arrow arr => Lens' d r -> (r -> r) -> arr d d
recordArrow' field f = arr $ field %~ f

The %~ is an update operator which updates a values inside a larger data structures using a function via the given lens.

Now the problem is that you don't automatically get lenses for your record fields, but you can either manually define them or generate them automatically using Template Haskell. For example

{-# LANGUAGE TemplateHaskell #-}

import Control.Lens

data Testdata = Testdata { _record1::Int, _record2::Int, _record3::Int }

makeLenses ''Testdata

Note that the original record accessors are prefixed with underscores so that we can use the original names for the lenses.

testRecord :: Testdata -> Testdata
testRecord =
       recordArrow' record1 id
   >>> recordArrow' record2 id
   >>> recordArrow' record3 id

The unarrow function is not needed. It's better to force a generic type to a concrete type by simple giving a type signature.

Note that if you are just looking for a nicer syntax to chain record operations and don't have any other use for arrows, you might prefer just to use the State monad with lenses. For example:

import Control.Monad.State (execState)

testRecord' :: Testdata -> Testdata
testRecord' = execState $ do
    record1 .= 3
    record2 %= (+5)
    record3 += 2
like image 138
shang Avatar answered Sep 22 '22 00:09

shang