How to store arbitrary values ​​in a recursive structure, or how to build an extensible software architecture?

I am working on a basic UI toolkit and am trying to understand the overall architecture.

I am considering using the WAI framework for extensibility . An abbreviated example of the main structure of my UI:

run :: Application -> IO ()
type Application = Event -> UI -> (Picture, UI)
type Middleware = Application -> Application


In WAI, arbitrary values ​​for Middleware are stored in the storage . I think this is a bad hack for storing harsh values ​​because it is not transparent, but I cannot think of a simple enough structure to replace this store to give each middleware a place to store arbitrary values.

I decided to recursively store tuples in tuples:

run :: (Application, x) -> IO ()
type Application = Event -> UI -> (Picture, UI)
type Middleware y x = (Application, x) -> (Application, (y,x))


Or, use only lazy lists to provide a level where there is no need to separate values ​​(which provides more freedom, but also has more problems):

run :: Application -> IO ()
type Application = [Event -> UI -> (Picture, UI)]
type Middleware = Application -> Application


Actually, I would use a modified lazy list solution. What other solutions might work?

Note that:

  • I prefer not to use a lens at all.
  • I know what UI -> (Picture, UI)

    can be defined as State UI Picture

  • I don't know of a solution regarding monads, transformers or FRP. It would be great to see him.

source to share

1 answer

Objectives provide a general way to reference data type fields so that you can expand or refactor your dataset without breaking backward compatibility. I will use libraries lens-family

and lens-family-th

to illustrate this as they are lighter dependencies than lens


Let's start with a simple post with two fields:

{-# LANGUAGE Template Haskell #-}

import Lens.Family2
import Lens.Family2.TH

data Example = Example
    { _int :: Int
    , _str :: String

makeLenses ''Example
-- This creates these lenses:
int :: Lens' Example Int
str :: Lens' Example String


Now you can write State

ful code that references the fields of your data structure. You can use Lens.Family2.State.Strict

for this purpose:

import Lens.Family2.State.Strict

-- Everything here also works for `StateT Example IO`
example :: State Example Bool
example = do
    s <- use str     -- Read the `String`
    str .= s ++ "!"  -- Set the `String`
    int += 2         -- Modify the `Int`
    zoom int $ do     -- This sub-`do` block has type: `State Int Int`
        m <- get
        return (m + 1)


The main thing to note is that I can update my datatype and the above code will compile. Add a new field to Example

and everything will work:

data Example = Example
    { _int  :: Int
    , _str  :: String
    , _char :: Char

makeLenses ''Example
int  :: Lens' Example Int
str  :: Lens' Example String
char :: Lens' Example Char


However, we can take it a step further and completely refactor our type Example

like this:

data Example = Example
    { _example2 :: Example
    , _char     :: Char

data Example2 = Example2
    { _int2 :: Int
    , _str2 :: String

makeLenses ''Example
char     :: Lens' Example Char
example2 :: Lens' Example Example2

makeLenses ''Example2
int2  :: Lens' Example2 Int
str2  :: Lens' Example2 String


Do we need to break our old code? Not! All we need to do is add the following two lenses to support backward compatibility:

int :: Lens' Example Int
int = example2 . int2

str :: Lens' Example Char
str = example2 . str2


Now, all the old code still works without any changes, despite the intrusive refactoring of our type Example


In fact, this doesn't just work for recordings. You can do the same for sum types (aka. Algebraic data types or enumerations). For example, suppose we have this type:

data Example3 = A String | B Int

makeTraversals ''Example3
-- This creates these `Traversals'`:
_A :: Traversal' Example3 String
_B :: Traversal' Example3 Int


Many of the things we did with the types of sums can be similarly repeated in terms of Traversal'

s. There's a notable exception for pattern matching: it is actually possible to implement pattern matching with integrity check using Traversal

s, but it is verbose at the moment.

However, the same holds true: if you express all sum type operations in terms of Traversal'

s, you can reorganize your sum type significantly and just update the appropriate Traversal'

one to maintain backward compatibility.

Finally, note that the true counterparts of sum type constructors are Prism

(which allow you to construct values ​​using constructors in addition to pattern matching). They are not supported by the library family lens-family

, but they are provided lens

and you can implement them yourself using just a dependency profunctors

if you want.

Also, if you are wondering what the analog of the lens

new type is, it is a Iso'

, and this also requires minimal dependency profunctors


Also, everything I said works for referencing multiple fields of recursive types (using Fold

s). Literally anything you can imagine wanting to reference in a data type the other way around is a library lens




All Articles