суббота, 2 ноября 2019 г.

Transitions correctness checking in compile-time

Untitled

One of the accusations against the OOP is state problems. They are divided into several sub-kinds, one of them is problem of transitions correctness, i.e. control when a transition from one state to another state is incorrect and should be avoided.

People often say that no such problem in FP languages. It's not true, but such illusion exists because of explicit arguments flow from function to function. And this problem appears when all arguments collapse to one argument aggregating all others. In this case the situation is reducing to the case in OOP.

There are multiple canonical solutions - working in compile-time with type-checker help or in run-time:

Run-time solutions are:

  • FSM - the best solution
  • contracts - not so good, but OK
  • asserts - reduced version of contracts

For example, function close() makes sense only if something was opened (let's suppose the result of the open in file):

something like this. The same is possible in an any language.

Compile-time solutions maybe:

  • Linear types
  • Unification (see Prolog) - more general solution
  • Indexed monads (for Haskell, for example)
  • Simple data-types (like data Opened = Opened String; data Read = Read String)

So, if some function must be under such control then even if it does not expect arguments at least one argument must be presented always, it may be term without parameters.

Of course, such a solution exists a long time ago in the OOP. Let's simulate it with a simple functionality - open a file, read it, parse it to integer, close it.

So, we see that we have states: {opened, read, parsed, closed} and each state accepts only limited number of transitions:

open -> read | close
read -> parse | close
parse -> close

That is, each function allows only limited number of next functions in our flow: open allows only a call of read or close and so on. Graphically it looks like:

open ------->.
 |           |
 v           |
read ------->.
 |           |
 v           |
parse ------>.
             |
             v
           close

close is allowed after any state.

We can turn it upside down:

       close
         ^
         |
         +----- parse
         |        ^
         +----- read
         |        ^
         +----- open

and to treat it as classes hierarchy: classes parse, read, open includes common feature - close, they know how to do it, so they inherit the close. Each class has some methods (one or more) and methods expect arguments of these classes types. This will guard us from wrong transitions in compile-time. Actually, this solution is possible even in Python:

All classes implement methods - what they can do. And each of them additionally can close opened file, so they inherit Close class (except Open sure). Their methods are typed with explicit argument state which restricts allowed transition, also it keeps data of the state. Pay attention that call p.close(r) for example, leads to "compile-time" (mypy checking, to be more accurate) error, i.e. transitions are explicit: Parse does not allow a parsing immediately after an opening of the file, without reading of it: p = Parse().parse(o):

error: Argument 1 to "parse" of "Parse" has incompatible type "Open"; expected "Read"
Found 1 error in 1 file (checked 1 source file)

A little improvement maybe to keep Open as instance attribute .opened to allow to close the file from any state, keeping the result of the opening (or something similar).

Classical pattern helps us even better than modern FP languages like Haskell where developers use indexed monads very rarely (and do miss linear types).

But there are not OOP languages where this is solving on type-level too: ATS, Clean, F*...

Комментариев нет:

Отправить комментарий

Thanks for your posting!