on master help sign in
Add ST tutorial section on DataStore interface
662e247Edwin Brady
docs/st/composing.rst+8
@@ -4,3 +4,11 @@
4Composing State Machines
4Composing State Machines
5************************
5************************
6
6
7+Adding a login fail counter to the data store
8+(horizontal composition)
9+
10+Vertical composition (what example?)
11+
12+Composite resources: Graphics
13+
14+
docs/st/introduction.rst+25
@@ -39,8 +39,33 @@ secondly, we can implement one state transition system in terms of others.
39Introductory example: a data store requiring a login
39Introductory example: a data store requiring a login
40====================================================
40====================================================
41
41
42+Many software components rely on some form of state, and there may be
43+operations which are only valid in specific states. For example, consider
44+a secure data store in which a user must log in before getting access to
45+some secret data. This system can be in one of two states:
46+
47+* ``LoggedIn``, in which the user is allowed to read the secret
48+* ``LoggedOut``, in which the user has no access to the secret
49+
50+We can provide commands to log in, log out, and read the data, as illustrated
51+in the following diagram:
52+
42|login|
53|login|
43
54
55+The ``login`` command, if it succeeds, moves the overall system state from
56+``LoggedOut`` to ``LoggedIn``. The ``logout`` command moves the state from
57+``LoggedIn`` to ``LoggedOut``. Most importantly, the ``readSecret`` command
58+is only valid when the system is in the ``LoggedIn`` state.
59+
60+We routinely use type checkers to ensure that variables and arguments are used
61+consistently. However, statically checking that operations are performed only
62+on resources in an appropriate state is not well supported by mainstream type
63+systems. In the data store example, for example, it's important to check that
64+the user is successfully logged in before using ``readSecret``. The
65+``ST`` library allows us to represent this kind of *protocol* in the type
66+system, and ensure at *compile-time* that the secret is only read when the
67+user is logged in.
68+
44Outline
69Outline
45=======
70=======
46
71
docs/st/machines.rst+382
@@ -4,3 +4,385 @@
4State Machines in Types
4State Machines in Types
5***********************
5***********************
6
6
7+In the introduction, we saw the following state transition diagram representing
8+the (abstract) states of a data store, and the actions we can perform on the
9+store:
10+
11+|login|
12+
13+We say that these are the *abstract* states of the store, because the concrete
14+state will contain a lot more information: for example, it might contain
15+user names, hashed passwords, the store contents, and so on. However, as far
16+as we are concerned for the actions ``login``, ``logout`` and ``readSecret``,
17+it's whether we are logged in or not which affects which are valid.
18+
19+We've seen how to manipulate states using ``ST``, and some small examples
20+of dependent types in states. In this section, we'll see how to use
21+``ST`` to provide a safe API for the data store. In the API, we'll encode
22+the above diagram in the types, in such a way that we can only execute the
23+operations ``login``, ``logout`` and ``readSecret`` when the state is
24+valid.
25+
26+So far, we've used ``State`` and the primitive operations, ``new``, ``read``,
27+``write`` and ``delete`` to manipulate states. For the data store API,
28+however, we'll begin by defining an *interface* (see :ref:`sect-interfaces` in
29+the Idris tutorial) which describes the operations on the store, and explains
30+in their types exactly when each operation is valid, and how it affects
31+the store's state. By using an interface, we can be sure that
32+this is the *only* way to access the store.
33+
34+Defining an interface for the data store
35+========================================
36+
37+We'll begin by defining a data type which represents the two abstract states
38+of the store, either ``LoggedOut`` or ``LoggedIn``:
39+
40+.. code-block:: idris
41+
42+ data Access = LoggedOut | LoggedIn
43+
44+We can define a data type for representing the current state of a store,
45+holding all of the necessary information (this might be user names, hashed
46+passwords, store contents and so on) and parameterise it by the logged in
47+status of the store:
48+
49+.. code-block:: idris
50+
51+ Store : Access -> Type
52+
53+Rather than defining a concrete type now, however, we'll include this in
54+a data store *interface* and define a concrete type later:
55+
56+.. code-block:: idris
57+
58+ interface DataStore (m : Type -> Type) where
59+ Store : Access -> Type
60+
61+We can continue to populate this interface with operations on the store. Among
62+other advantages, by separating the *interface* from its *implementation* we
63+can provide different concrete implementations for different contexts.
64+Furthermore, we can write programs which work with a store without needing
65+to know any details of how the store is implemented.
66+
67+We'll need to be able to ``connect`` to a store, and ``disconnect`` when
68+we're done. Add the following methods to the ``DataStore`` interface:
69+
70+.. code-block:: idris
71+
72+ connect : ST m Var [add (Store LoggedOut)]
73+ disconnect : (store : Var) -> ST m () [remove store (Store LoggedOut)]
74+
75+The type of ``connect`` says that it returns a new resource which has the
76+initial type ``Store LoggedOut``. Conversely, ``disconnect``, given a
77+resource in the state ``Store LoggedOut``, removes that resource.
78+We can see more clearly what ``connect`` does by trying the following
79+(incomplete) definition:
80+
81+.. code-block:: idris
82+
83+ doConnect : DataStore m => ST m () []
84+ doConnect = do st <- connect
85+ ?whatNow
86+
87+Note that we're working in a *generic* context ``m``, constrained so that
88+there must be an implementation of ``DataStore`` for ``m`` to be able to
89+execute ``doConnect``.
90+If we check the type of ``?whatNow``, we'll see that the remaining
91+operations begin with a resource ``st`` in the state ``Store LoggedOut``,
92+and we need to finish with no resources.
93+
94+.. code-block:: idris
95+
96+ m : Type -> Type
97+ constraint : DataStore m
98+ st : Var
99+ --------------------------------------
100+ whatNow : STrans m () [st ::: Store LoggedOut] (\result => [])
101+
102+Then, we can remove the resource using ``disconnect``:
103+
104+.. code-block:: idris
105+
106+ doConnect : DataStore m => ST m () []
107+ doConnect = do st <- connect
108+ disconnect st
109+ ?whatNow
110+
111+Now checking the type of ``?whatNow`` shows that we have no resources
112+available:
113+
114+.. code-block:: idris
115+
116+ m : Type -> Type
117+ constraint : DataStore m
118+ st : Var
119+ --------------------------------------
120+ whatNow : STrans m () [] (\result => [])
121+
122+To continue our implementation of the ``DataStore`` interface, next we'll add a
123+method for reading the secret data. This requires that the ``store`` is in the
124+state ``Store LoggedIn``:
125+
126+.. code-block:: idris
127+
128+ readSecret : (store : Var) -> ST m String [store ::: Store LoggedIn]
129+
130+At this point we can try writing a function which connects to a store,
131+reads the secret, then disconnects. However, it will be unsuccessful, because
132+``readSecret`` requires us to be logged in:
133+
134+.. code-block:: idris
135+
136+ badGet : DataStore m => ST m () []
137+ badGet = do st <- connect
138+ secret <- readSecret st
139+ disconnect st
140+
141+This results in the following error, because ``connect`` creates a new
142+store in the ``LoggedOut`` state, and ``readSecret`` requires the store
143+to be in the ``LoggedIn`` state:
144+
145+.. code-block:: idris
146+
147+ When checking an application of function Control.ST.>>=:
148+ Error in state transition:
149+ Operation has preconditions: [st ::: Store LoggedOut]
150+ States here are: [st ::: Store LoggedIn]
151+ Operation has postconditions: \result => []
152+ Required result states here are: \result => []
153+
154+The error message explains how the required input states (the preconditions)
155+and the required output states (the postconditions) differ from the states
156+in the operation. In order to use ``readSecret``, we'll need a way to get
157+from a ``Store LoggedOut`` to a ``Store LoggedIn``. As a first attempt,
158+we can try the following type for ``login``:
159+
160+.. code-block:: idris
161+
162+ login : (store : Var) -> ST m () [store ::: Store LoggedOut :-> Store LoggedIn] -- Incorrect type!
163+
164+Note that in the *interface* we say nothing about *how* ``login`` works;
165+merely how it affects the overall state. Even so, there is a problem with
166+the type of ``login``, because it makes the assumption that it will always
167+succeed. If it fails - for example because the implementation prompts for
168+a password and the user enters the password incorrectly - then it must not
169+result in a ``LoggedIn`` store.
170+
171+Instead, therefore, ``login`` will return whether logging in was successful,
172+via the following type;
173+
174+.. code-block:: idris
175+
176+ data LoginResult = OK | BadPassword
177+
178+Then, we can *calculate* the result state (see :ref:`depstate`) from the
179+result. Add the following method to the ``DataStore`` interface:
180+
181+.. code-block:: idris
182+
183+ login : (store : Var) ->
184+ ST m LoginResult [store ::: Store LoggedOut :->
185+ (\res => Store (case res of
186+ OK => LoggedIn
187+ BadPassword => LoggedOut))]
188+
189+If ``login`` was successful, then the state after ``login`` is
190+``Store LoggedIn``. Otherwise, the state is ``Store LoggedOut``.
191+
192+To complete the interface, we'll add a method for logging out of the store.
193+We'll assume that logging out is always successful, and moves the store
194+from the ``Store LoggedIn`` state to the ``Store LoggedOut`` state.
195+
196+.. code-block:: idris
197+
198+ logout : (store : Var) -> ST m () [store ::: Store LoggedIn :-> Store LoggedOut]
199+
200+This completes the interface, repeated in full for reference below:
201+
202+.. code-block:: idris
203+
204+ interface DataStore (m : Type -> Type) where
205+ Store : Access -> Type
206+
207+ connect : ST m Var [add (Store LoggedOut)]
208+ disconnect : (store : Var) -> ST m () [remove store (Store LoggedOut)]
209+
210+ readSecret : (store : Var) -> ST m String [store ::: Store LoggedIn]
211+ login : (store : Var) ->
212+ ST m LoginResult [store ::: Store LoggedOut :->
213+ (\res => Store (case res of
214+ OK => LoggedIn
215+ BadPassword => LoggedOut))]
216+ logout : (store : Var) -> ST m () [store ::: Store LoggedIn :-> Store LoggedOut]
217+
218+Before we try creating any implementations of this interface, let's see how
219+we can write a function with it, to log into a data store, read the secret
220+if login is successful, then log out again.
221+
222+Writing a function with the data store
223+======================================
224+
225+As an example of working with the ``DataStore`` interface, we'll write a
226+function ``getData``, which connects to a store in order to read some data from
227+it. We'll write this function interactively, step by step, using the types of
228+the operations to guide its development. It has the following type:
229+
230+.. code-block:: idris
231+
232+ getData : (ConsoleIO m, DataStore m) => ST m () []
233+
234+This type means that there are no resources available on entry or exit.
235+That is, the overall list of actions is ``[]``, meaning that at least
236+externally, the function has no overall effect on the resources. In other
237+words, for every resource we create during ``getData``, we'll also need to
238+delete it before exit.
239+
240+Since we want to use methods of the ``DataStore`` interface, we'll
241+constraint the computation context ``m`` so that there must be an
242+implementation of ``DataStore``. We also have a constraint ``ConsoleIO m``
243+so that we can display any data we read from the store, or any error
244+messages.
245+
246+We start by connecting to the store, creating a new resource ``st``, then
247+trying to ``login``:
248+
249+.. code-block:: idris
250+
251+ getData : (ConsoleIO m, DataStore m) => ST m () []
252+ getData = do st <- connect
253+ ok <- login st
254+ ?whatNow
255+
256+Logging in will either succeed or fail, as reflected by the value of
257+``ok``. If we check the type of ``?whatNow``, we'll see what state the
258+store currently has:
259+
260+.. code-block:: idris
261+
262+ m : Type -> Type
263+ constraint : ConsoleIO m
264+ constraint1 : DataStore m
265+ st : Var
266+ ok : LoginResult
267+ --------------------------------------
268+ whatNow : STrans m () [st ::: Store (case ok of
269+ OK => LoggedIn
270+ BadPassword => LoggedOut)]
271+ (\result => [])
272+
273+The current state of ``st`` therefore depends on the value of ``ok``,
274+meaning that we can make progress by case splitting on ``ok``:
275+
276+.. code-block:: idris
277+
278+ getData : (ConsoleIO m, DataStore m) => ST m () []
279+ getData = do st <- connect
280+ ok <- login st
281+ case ok of
282+ OK => ?whatNow_1
283+ BadPassword => ?whatNow_2
284+
285+The types of the holes in each branch, ``?whatNow_1`` and ``?whatNow_2``,
286+show how the state changes depending on whether logging in was successful.
287+If it succeeded, the store is ``LoggedIn``:
288+
289+.. code-block:: idris
290+
291+ --------------------------------------
292+ whatNow_1 : STrans m () [st ::: Store LoggedIn] (\result => [])
293+
294+On the other hand, if it failed, the store is ``LoggedOut``:
295+
296+.. code-block:: idris
297+
298+ --------------------------------------
299+ whatNow_2 : STrans m () [st ::: Store LoggedOut] (\result => [])
300+
301+In ``?whatNow_1``, since we've successfully logged in, we can now read
302+the secret and display it to the console:
303+
304+.. code-block:: idris
305+
306+ getData : (ConsoleIO m, DataStore m) => ST m () []
307+ getData = do st <- connect
308+ ok <- login st
309+ case ok of
310+ OK => do secret <- readSecret st
311+ putStrLn ("Secret is: " ++ show secret)
312+ ?whatNow_1
313+ BadPassword => ?whatNow_2
314+
315+We need to finish the ``OK`` branch with no resources available. We can
316+do this by logging out of the store then disconnecting:
317+
318+.. code-block:: idris
319+
320+ getData : (ConsoleIO m, DataStore m) => ST m () []
321+ getData = do st <- connect
322+ ok <- login st
323+ case ok of
324+ OK => do secret <- readSecret st
325+ putStrLn ("Secret is: " ++ show secret)
326+ logout st
327+ disconnect st
328+ BadPassword => ?whatNow_2
329+
330+Note that we *must* ``logout`` of ``st`` before calling ``disconnect``,
331+because ``disconnect`` requires that the store is in the ``LoggedOut``
332+state.
333+
334+Furthermore, we can't simply use ``delete`` to remove the resource, as
335+we did with the ``State`` examples in the previous section, because
336+``delete`` only works when the resource has type ``State ty``, for some
337+type ``ty``. If we try to use ``delete`` instead of ``disconnect``, we'll
338+see an error message like the following:
339+
340+.. code-block:: idris
341+
342+ When checking argument prf to function Control.ST.delete:
343+ Can't find a value of type
344+ InState st (State st) [st ::: Store LoggedOut]
345+
346+In other words, the type checker can't find a proof that the resource
347+``st`` has a type of the form ``State st``, because its type is
348+``Store LoggedOut``. Since ``Store`` is part of the ``DataStore`` interface,
349+we *can't* yet know the concrete representation of the ``Store``, so we
350+need to remove the resource via the interface, with ``disconnect``, rather
351+than directly with ``delete``.
352+
353+We can complete ``getData`` as follows, using a pattern matching bind
354+alternative (see the Idris tutorial, :ref:`monadsdo`) rather than a
355+``case`` statement to catch the possibilty of an error with ``login``:
356+
357+.. code-block:: idris
358+
359+ getData : (ConsoleIO m, DataStore m) => ST m () []
360+ getData = do st <- connect
361+ OK <- login st
362+ | BadPassword => do putStr "Failure\n"
363+ disconnect st
364+ secret <- readSecret st
365+ putStr ("Secret is: " ++ show secret ++ "\n")
366+ logout st
367+ disconnect st
368+
369+We can't yet try this out, however, because we don't have any implementations
370+of ``getData``! If we try to execute it in an ``IO`` context, for example,
371+we'll get an error saying that there's no implementation of ``DataStore IO``:
372+
373+.. code::
374+
375+ *Login> :exec run {m = IO} getData
376+ When checking an application of function Control.ST.run:
377+ Can't find implementation for DataStore IO
378+
379+The final step in implementing a data store which correctly follows the
380+state transition diagram, therefore, is to provide an implementation
381+of ``DataStore``.
382+
383+Implementing the interface
384+==========================
385+
386+
387+.. |login| image:: ../image/login.png
388+ :width: 500px
docs/st/state.rst+52−3
@@ -19,7 +19,7 @@ how those resources change when a function runs:
19A value of type ``STrans m resultType in_res out_res_fn`` represents a sequence
19A value of type ``STrans m resultType in_res out_res_fn`` represents a sequence
20of actions which can manipulate state. The arguments are:
20of actions which can manipulate state. The arguments are:
21
21
22−* ``m``, which is an underlying *context* in which the actions will be executed.
22+* ``m``, which is an underlying *computation context* in which the actions will be executed.
23 Usually, this will be a generic type with a ``Monad`` implementation, but
23 Usually, this will be a generic type with a ``Monad`` implementation, but
24 it isn't necessarily so. In particular, there is no need to understand monads
24 it isn't necessarily so. In particular, there is no need to understand monads
25 to be able to use ``ST`` effectively!
25 to be able to use ``ST`` effectively!
@@ -96,7 +96,7 @@ reads it back, then deletes it using ``delete``, returning the final value
96of the resource. Again, we'll see the types of ``new`` and ``delete``
96of the resource. Again, we'll see the types of ``new`` and ``delete``
97shortly.
97shortly.
98
98
99−The ``m`` argument to ``STrans`` (of type ``Type -> Type``) is the *context* in
99+The ``m`` argument to ``STrans`` (of type ``Type -> Type``) is the *computation context* in
100which the function can be run. Here, the type level variable indicates that we
100which the function can be run. Here, the type level variable indicates that we
101can run it in *any* context. We can run it in the identity context with
101can run it in *any* context. We can run it in the identity context with
102``runPure``. For example, try entering the above definitions in a file
102``runPure``. For example, try entering the above definitions in a file
@@ -166,6 +166,25 @@ The ``lift`` function allows us to use funtions from the underlying
166computation context (``IO`` here) directly. Again, we'll see the exact type
166computation context (``IO`` here) directly. Again, we'll see the exact type
167of ``lift`` shortly.
167of ``lift`` shortly.
168
168
169+.. topic:: !-notation
170+
171+ In ``ioMakeAndIncrement`` we've used ``!(read var)`` to read from the
172+ resource. You can read about this ``!``-notation in the main Idris tutorial
173+ (see :ref:`monadsdo`). In short, it allows us to use an ``STrans``
174+ function inline, rather than having to bind the result to a variable
175+ first.
176+
177+ Conceptually, at least, you can think of it as having the following type:
178+
179+ .. code-block:: idris
180+
181+ (!) : STrans m a state_in state_out -> a
182+
183+ It is syntactic sugar for binding a variable immediately before the
184+ current action in a ``do`` block, then using that variable in place of
185+ the ``!``-expression.
186+
187+
169In general, though, it's bad practice to use a *specific* context like
188In general, though, it's bad practice to use a *specific* context like
170``IO``. Firstly, it requires us to sprinkle ``lift`` liberally throughout
189``IO``. Firstly, it requires us to sprinkle ``lift`` liberally throughout
171our code, which hinders readability. Secondly, and more importantly, it will
190our code, which hinders readability. Secondly, and more importantly, it will
@@ -229,6 +248,8 @@ can execute ``ioMakeAndIncrement`` at the REPL as follows:
229 var = 93
248 var = 93
230 var = 94
249 var = 94
231
250
251+.. _depstate:
252+
232Manipulating ``State`` with dependent types
253Manipulating ``State`` with dependent types
233===========================================
254===========================================
234
255
@@ -550,7 +571,8 @@ given input and output resource lists in full. We can do this with
550
571
551``ST`` is a type level function which computes an appropriate ``STrans``
572``ST`` is a type level function which computes an appropriate ``STrans``
552type given a list of *actions*, which describe transitions on resources.
573type given a list of *actions*, which describe transitions on resources.
553−An ``Action`` in a function type can take one of the following forms:
574+An ``Action`` in a function type can take one of the following forms (plus
575+some others which we'll see shortly):
554
576
555* ``lbl ::: ty`` expresses that the resource ``lbl`` begins and ends in
577* ``lbl ::: ty`` expresses that the resource ``lbl`` begins and ends in
556 the state ``ty``
578 the state ``ty``
@@ -596,6 +618,33 @@ update depending on a result, as with ``readAndAdd``, then we need to describe
596it in full. Otherwise, as with ``increment`` and ``makeAndIncrement``, we can
618it in full. Otherwise, as with ``increment`` and ``makeAndIncrement``, we can
597write the input and output resource lists without repetition.
619write the input and output resource lists without repetition.
598
620
621+An ``Action`` can also describe *adding* and *removing* states:
622+
623+* ``add ty``, assuming the operation returns a ``Var``, adds a new resource
624+ of type ``ty``.
625+* ``remove lbl ty`` expresses that the operation removes the resource named
626+ ``lbl``, beginning in state ``ty`` from the resource list.
627+
628+So, for example, we can write:
629+
630+.. code-block:: idris
631+
632+ newState : ST m Var [add (State Int)]
633+ removeState : (lbl : Var) -> ST m () [remove lbl (State Int)]
634+
635+The first of these, ``newState``, returns a new resource label, and adds that
636+resource to the list with type ``State Int``. The second, ``removeState``,
637+given a label ``lbl``, removes the resource from the list. These types are
638+equivalent to the following:
639+
640+.. code-block:: idris
641+
642+ newState : STrans m Var [] (\lbl => [lbl ::: State Int])
643+ removeState : (lbl : Var) -> STrans m () [lbl ::: State Int] (const [])
644+
645+These are the primitive methods of constructing an ``Action``. Later, we will
646+encounter some other ways using type level functions to help with readability.
647+
599In the remainder of this tutorial, we will generally use ``ST`` except on
648In the remainder of this tutorial, we will generally use ``ST`` except on
600the rare occasions we need the full precision of ``STrans``. In the next
649the rare occasions we need the full precision of ``STrans``. In the next
601section, we'll see how to use the facilities provided by ``ST`` to write
650section, we'll see how to use the facilities provided by ``ST`` to write
docs/tutorial/interfaces.rst+2
@@ -203,6 +203,8 @@ abstracts the notion of function application:
203 pure : a -> f a
203 pure : a -> f a
204 (<*>) : f (a -> b) -> f a -> f b
204 (<*>) : f (a -> b) -> f a -> f b
205
205
206+.. _monadsdo:
207+
206Monads and ``do``-notation
208Monads and ``do``-notation
207==========================
209==========================
208
210