Most Haskellers would like to use a mature FRP-based GUI.
I think FRP may not be the best tool for special user interfaces, like interfaces which consists of buttons, checkboxes, combo boxes, text entries and menus only.
I present a prototype lens-based model which fits better these user interfaces.
First I would like to show some screenshots of a demo application. It has separate demos in its tabs.
While reading the features of the demos, think about how much lines would you need to code these demos in your favourite programming language and GUI framework.
You can also try the demo application, the needed steps are:
cabal install gtk2hs-buildtools
cabal install lgtk
lgtkdemo
Features:
DeleteAll
and DelSel
buttons shows how many items would be deleted.Features:
Features:
Leaf
and Node
again, you get back the previous state.The same editor with checkboxes:
The Int list editor is less than 70 lines in LGtk.
I did not count the generic parts, like a generic undo-redo state transformation.
The addition relation editor is 20 lines in LGtk.
The binary tree shape editor is 10 lines in LGtk.
The lens-based Gtk interface has the following ingredients:
The data type of monadic lenses is:
newtype MLens m a b
= MLens (a -> m (b, b -> m a))
Side-effect free lenses have type either
type Lens a b = MLens Identity a b
or
type Lens a b = forall m . Monad m => MLens m a b
Monadic lenses are handy because they contain the well-known references, for example IO references:
type Ref m a = MLens m () a
In this way references can easily be composed with lenses.
For example, if
r :: Ref m (a,b)
then
fstLens . r :: Ref m a
Note that LGtk use lots of impure lenses (lenses which do not fulfil the lens laws).
Using lenses which do not fulfil the lens laws are safe, but one should take extra care when doing program transformations or reasoning about code with impure lenses.
NewRef
classMonads with reference creation can be given the NewRef
class instance:
class (Monad m) => NewRef m where
newRef :: a -> m (Ref m a)
instance NewRef IO
ExtRef
classSuppose that we would like to extend a reference with a hidden state.
You can think of this operation as backward application of a lens to a reference.
This operation can be done in the NewRef
class:
class NewRef m => ExtRef m where
extRef :: Ref m b -> MLens m a b -> a -> m (Ref m a)
Explanation of extRef
:
Suppose that k
is a pure lens, and
s <- extRef r k a0
Then following laws should hold:
(k . s)
behaves exactly as r
.s
is the result of (readRef r >>= setL k a0)
.Moreover, (extRef r k a0)
should not change the value of r
.
It is better to explain this on concrete examples.
I implemented the following tests:
newRefTest = runTest $ do
r <- newRef 3 -- create a new reference with initial value 3
r ==> 3 -- reading the reference should give 3
writeRefTest = runTest $ do
r <- newRef 3
r ==> 3 -- value before write
writeRef r 4
r ==> 4 -- value after write
extRefTest = runTest $ do
r <- newRef $ Just 3 -- r is a (Maybe Int) reference
q <- extRef r maybeLens (False, 0)
-- we extend r's state; maybeLens :: Lens m (Bool, a) (Maybe a)
let q1 = fstLens . q -- lens composition
q2 = sndLens . q
r ==> Just 3 -- r is still (Just 3)
q ==> (True, 3) -- q is (True, 3)
writeRef r Nothing
r ==> Nothing
q ==> (False, 3) -- q still holds the value 3
q1 ==> False
writeRef q1 True
r ==> Just 3 -- we have got back (Just 3)
writeRef q2 1
r ==> Just 1
joinTest = runTest $ do
r2 <- newRef 5
r1 <- newRef 3
rr <- newRef r1
r1 ==> 3
let r = joinLens rr -- lenses can be joined, which is really handy for dynamic interfaces
r ==> 3
writeRef r1 4
r ==> 4
writeRef rr r2 -- switching to another source
r ==> 5
writeRef r1 4
r ==> 5
writeRef r2 14
r ==> 14
With the help of ExtRef
, one can define undo-redo state transformation (I don’t give more details now):
undoTr
:: ExtRef m =>
(a -> a -> Bool) -- equality on state
-> Ref m a -- reference of state
-> m ( m (Maybe (m ())) -- undo action, Nothing if it has no sense
, m (Maybe (m ())) -- redo action, Nothing if it has no sense
)
Now, the Gtk API is that simple (this is a prototype without styling):
runI :: I IO -> IO () -- we need only one rendering function
data I m
= Label (m String) -- ^ label
| Button { label :: m String
, action :: m (Maybe (m ())) -- ^ when the @Maybe@ value is @Nothing@, the button is inactive
} -- ^ button
| Checkbox (Ref m Bool) -- ^ checkbox
| Entry (Ref m String) -- ^ entry field
-- ...
| List ListLayout [I m] -- ^ group interfaces into row or column
| forall a . Eq a
=> Cell { underlying_value :: m a
, dynamic_interface :: a -> I m
} -- ^ dynamic interface
| Action (m (I m)) -- ^ do an action before giving the interface
data ListLayout
= Horizontal | Vertical
In the actual interface I use also free monads to speed up the implementation.
Every feedback is appreciated, especially the following: