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 asState UI Picture
. - I don't know of a solution regarding monads, transformers or FRP. It would be great to see him.
source to share
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
.
source to share