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?
- 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
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
to illustrate this as they are lighter dependencies than
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
ful code that references the fields of your data structure. You can use
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
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
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
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
s. There's a notable exception for pattern matching: it is actually possible to implement pattern matching with integrity check using
s, but it is verbose at the moment.
However, the same holds true: if you express all sum type operations in terms of
s, you can reorganize your sum type significantly and just update the appropriate
one to maintain backward compatibility.
Finally, note that the true counterparts of sum type constructors are
(which allow you to construct values using constructors in addition to pattern matching). They are not supported by the library family
, but they are provided
and you can implement them yourself using just a dependency
if you want.
Also, if you are wondering what the analog of the
new type is, it is a
, and this also requires minimal dependency
Also, everything I said works for referencing multiple fields of recursive types (using
s). Literally anything you can imagine wanting to reference in a data type the other way around is a library
source to share