Applicative Computation Expressions - 3
this post is part of the F# Advent Calendar 2020
In this third installment of our series about applicative computation experessions, we'll jump to practice with Lego Mindstorm Ev3. This should demonstrate a non trivial, real life use case for applicatives.
Even if you don't have a mindstorm set at home, follow this post. We'll mix binary protocols and category theory - lightly explained - to provide a nice and safe API.
Mindstorm control over bluetooth
A few years back, I wrote a quick fsx script to control lego mindstorm in F#. I worked again on it recently to port it to net5.0.
Once paired and connected, the brick - the central component of Mindstom used to drive motors and read sensors - appears as a serial port, COM9 on my machine. If you're using a linux system, it should appear as /dev/ttyS9 (or another number).
System.IO.SerialPort
is available directly in Net framework, and as a nuget in netcore and net5.0.
Add the reference to your project, or use the #r nuget reference in a F# 5.0 script:
1:
|
|
We will read/write bytes using the SerialPort
and ReadOnlyMemory
buffers. We'll soon
go in more details.
Request/Response
The most interesting part is the Request/Response mechanisme used for
sensors. We write the request on the serial port by using port.Write
, but get the
response through an notification of the port.DataReceived
event.
The request response is asynchronous, and it would be far easier to present it in the API as an Async construct.
When sending a request, we must include a mandatory sequence number in int. This sequence number will the be transmited in the corresponding response for correlation.
1:
|
|
We will use this sequence number and a MailboxProcessor
to implement Async in a responseDispatcher.
It accepts two kinds of messages.
Request
contains a Sequence number and a callback to call
when the response bytes are available.
Forward
contains a Sequence number and corresponding bytes.
1: 2: 3: 4: 5: 6: 7: 8: 9: |
|
The mailbox processor is using an async rec function to process messages. On a Request
message,
it adds the callback to a Map
under the sequence number key. On a Forward
message,
it finds the callback corresponding to the sequence number and call it with the data:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: |
|
The callback in the Request
message is create using PostAndAsyncReply
. The result
is an Async that is competed when the callback is called.
With this in place we can implement the Brick:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50: 51: 52: |
|
Commands Serialization
Now that we can easily send bytes over the serial port, we need to serialize commands.
the format is :
1: 2: 3: 4: 5: |
|
It is possible to send several commands in a single call.
Command type
The command type is encoded on a single byte. bit 0 indicates whether it's a direct command (activating motors/sensors) or system command (uploading/downloading files). Bit 7 indicates whether we expect a reply. We'll not use system commands in this post.
1: 2: 3: 4: 5: |
|
OpCode
The type of command is indicated by a 1 or 2 bytes opcode. Here is an enum of the different opcodes supported by Ev3:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: |
|
and the list of system opcodes (not used)
1: 2: 3: 4: 5: 6: |
|
To help with the serialization in a Span
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: |
|
Each of these functions write a value at the beginning of the span and slice the appropriate size to return a new span starting just after the value that was just written.
Using this was can write the serializeOpcode function:
1: 2: 3: 4: 5: 6: 7: 8: |
|
In order to allocate the buffer, we need to compute the total size. The following functions returns the size for an opcode:
1: 2: |
|
Parameters
Each opcode is followed by a given number of parameters containing the mandatory values for the operation.
We define a union type to represents different parameter types and their values:
1: 2: 3: 4: 5: 6: |
|
All types are pretty explicit except the last one. GlobalIndex is used to indicate a byte offset in a response data. We'll use it later.
Each argument is composed of a 1 byte prefix indicating it's type/size, followed by the value itself. Here are the different prefixes:
1: 2: 3: 4: 5: 6: |
|
Now we can serialize a parameter and get its length:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: |
|
So we can define a command as a union:
1: 2: 3: 4: |
|
Computing the size of a command is fairly straightforward:
1: 2: 3: 4: |
|
The serialization is a bit more complicated if we want to loop on parameters
using a recursive function. The reason is that F# doesn't currently support
Span<T> -> Span<T>
function parameters. This signature is used by our serializers
that take a Span, write at the begining an returns a shorter span starting after
writen bytes. To wrokaround this problem we have to use a plain delegate:
1: 2: 3: 4: 5: 6: 7: 8: |
|
The serializeAll
function takes a Serializer, a list of values and a buffer.
It serializes all elements in the list using the serializer and returns a span
that starts just after the bytes we just wrote.
We use it to implement serializeCommand
:
1: 2: 3: 4: 5: |
|
Putting it all together
We can now proceed to the serialize
function that allocates memory from the MemoryPool
,
writes each part using the functions we defined above, and returns a memory owner.
The memory owner has to be disposed to return the memory to the pool.
It takes a command list and serialize all the commands using serializeAll:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: |
|
And we write a send
function that takes a list of commands, serialize them and
sends the result to the brick:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: |
|
Defining commands
We're now ready to define commands in a more friendly way.
For instance the startMotor
command can be called
with a list of output ports (the ones on the top of the brick calles A, B, C and D):
1: 2: 3: 4: 5: |
|
Ports are encoded as a Byte parameter with bit 0 set for A, 1 for B, 2 for C, and 3 for D:
1: 2: 3: 4: 5: 6: 7: 8: 9: |
|
startMotor
is using the OutputStart
opcode followed by two parameters: a 0 byte , and a port byte.
1: 2: 3: |
|
stopMotor
is using the OutputStop
opcode followed by the same parameters, and an extra Break value encoded
as a 0 or 1 byte:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: |
|
Here are a few more commands:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: |
|
You can try to put a few commands in the list and send
them to the brick. It should
work.
Computation Expression
To make it better, we continue with a lego
computation expression that builds a full
program taking commands and async local computations.
It is used to compose functions that takes a Brick and return an async:
1:
|
|
This is a classic Computation expression (not an applicative).
1:
|
|
The Bind method takes a Lego<'a>
and a function that takes a 'a
as an input to return a Lego<'b>
.
The result should be a Lego<'b>
, so we write a lambda that takes a Brick.
We pass this brick to x, to get an Async<'a>
, we pass the value from this async to f using
async.Bind
.
1: 2: 3: |
|
This overload does quite the same but directly takes a Command list and send it
1: 2: 3: |
|
Same thing for a single command
1: 2: 3: |
|
We can also do it for simple Async<'a>
(for instance Async.Sleep(1000)
).
In this case we just bind it directly.
1: 2: 3: |
|
Return
creates a Lego<'a>
from a value. It just takes a brick that it will not use
and returns an Async with the value.
1:
|
|
Other methods implement return!, for, combination sequenced calls, etc.
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: |
|
The run
functions takes a Lego
1: 2: 3: 4: |
|
The runSynchronously
functions takes a Lego<'a> function, and runs it synchronously
1: 2: |
|
We choose lego
for our Computation Expression name
1:
|
|
Let's play
Here is a sample that connects to COM9 and repeat 3 times a batch of commands to turn motor in port A in one direction for 1 second, then in the other direction for another second. It stops motors once done.
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: |
|
Sensors
The Ev3 comes with 3 default sensors, Color, Touch and IR. Extra gyroscopic and ultrasound sensors can be acquierd separatly.
Sensors are connected to input ports (1 to 4). Motor ports (A to D) can also be read to get the rotation of the motors:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: |
|
Sensors can return different values
- Color, ambiant light, reflected light for the color sensor
- Touch can return pressed/released state or bumps
- IR can return proximity, and remote buttons state...
each of this mode correspond to a 1 byte value
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: |
|
Different units are used depending on the sensor value:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: |
|
Read command
Each of the datatypes correspond to a differents read opcode:
1: 2: 3: 4: 5: |
|
With this opcode we can create a read command as we did previously. The position
parameter
is the position of the corresponding value in the reponse bytes.
1: 2: 3: 4: 5: 6: 7: 8: |
|
For a single sensor request, it's easy, just pass 0
for position, and read the value
at the start of the response.
The send
function defined before was not waiting for a response. We create a request
function
the same way, but asks for a reply and use AsyncRequest
to get a response.
It checks the reply type (a byte just after response length).
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: |
|
To query multiple sensors at once, we can create multiple read commands with different positions, and send them in a single batch. It avoids multiple roundtrip through bluetooth which can be expensive. The problem is that setting response value position can be error prone.
With the readDataTypeLen
we can know the size of the value, and accumulate them to compute
the offset. A readLength
function that extracts the length from the triplet will make our life easier:
1:
|
|
We accumulate the sizes as offsets using a mapFold
. The accumulator in mapFold takes a state
(the offset) and an input (the sensor paramaters), and outputs a value (the the command built using the offset)
and a new state (the new offset computed by adding the value length).
This way we get a list of command with the rights offsets as well as the total length passed as globalSize.
To interpret the result, we use mapFold
again. Now the state is a ReadOnlyMemory<byte>
starting at the 3rd byte (to skip the size and response status, the reason for the Slice(3)
).
For each item, we compute its length. The item is a pair of the sensors parameter and a slice
containing the value. The state is sliced to skip the bytes from current value and start at the next one.
We combine it with a call to request to have this function that takes a brick and a list of
sensor parameters and returns a map of sensor parameters and their value as a ReadOnlyMemory<byte>
.
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: |
|
Applicative
We solved the problem of values offsets, but a few challenge remain. First the result is still in the form of a ReadOnlyMemory<byte>
,
and we should be carefull to read it accordingly to the requested data. Then we have to
be sure we don't try to look for values in the result that we did not request.
If you've read previous posts already, you'll recognize the Query<'t>
applicative. Here
we will create a Sensor<'t>
type:
1: 2: 3: 4: 5: |
|
It contains a set of inputs to request, and a function to extract a value of type 't
from the result of our readAux
function.
For instance the simplest way to create a sensor from an input:
1: 2: 3: |
|
It indicates that it should request the given input and gets it from the map. The result
will be a ReadOnlyMemory<byte>
.
Creating a read
functions that takes a Sensor<'t>
and calls readAux
is straightforwad:
1: 2: 3: 4: 5: 6: 7: 8: 9: |
|
Using a Sensor<'t>
and read
, no way to get it wrong.
Now we want to create more complex sensors by combining simple ones.
Ret
We can create a pure sensor, a sensor that request no inputs and return a given value:
1: 2: 3: |
|
It will be useful later.
Map
We can define a map
operation on it (yes, it's a functor). It takes a 'a -> 'b
functions
and changes a Sensor<'a>
to a Sensor<'b>
:
1: 2: 3: |
|
It requests the same inputs at the given sensor, gets its value and pass it to f.
Map2
More interesting, we can define a map2
that takes to sensors, and pass their values to
a function to compute a single result. This result will also be a sensor that request for
inputs of both, and combine their results using f:
1: 2: 3: 4: 5: 6: |
|
We can use map2
to zip
sensors. It takes to sensors and create a single sensor
that contains boths values as a tuple:
1:
|
|
Apply
The problem with map2, is that it works only with 2 sensors. For 3 sensors we would have to create a map3. And a map4, map5 etc.
But in F#, every function is a function of 1 argument due to currying. A function of type
int -> bool -> string
can be used as a int -> (bool -> string)
, it takes a single int
argument and returns a function that takes a bool
and returns a string
.
Passing this function to map
with a sensor of type Sensor<int>
, we get a
Sensor<bool -> string>
. The meaning of this signature can seem obscure at first.
It says that if you give this to the read
function above, it will make a request
and use the response values to build a bool -> string
result. if you pass a
bool
to this result, you get a string
.
But we would like the bool
to also be read from a sensor. So we have a Sensor<bool -> string>
and a Sensor<bool>
. We can obviously use map2
on this pair of sensors. map2
will pass the
values of the sensors to the function passed as an argument. The first argument of this function will
be a bool -> string
, the second a bool
. We can pass the bool
value to the bool -> string
function
to get a string
. The result will be a Sensor<string>
.
I used specific types to make it simpler by it works for any type:
1: 2: |
|
Typed basic sensors
For a given data type, the conversion should always be the same. We create a function for each data type.
Each of the inputXXX functions take a port and a mode are returns a typed sensor.
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: |
|
Using this functions we define more interesting sensors
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: |
|
Operators
Prior to F# 5.0 it is not possible to implement applicatives with computation expressions.
Instead we can define <!>
for map
and <*>
for apply
to get infix syntax.
<!>
takes a function on a left and a sensor on the right. We get a sensor with the argument
applied. If it was a simple 1 argument function, we get a sensor of the result:
1: 2: 3: 4: 5: 6: 7: |
|
If the function take several parameters, we get a sensor of a function as a result.
We can use it with <*>
, the operator for apply
:
1: 2: 3: 4: 5: 6: 7: 8: |
|
This is a composit sensors that gets the state from the Touch sensor in input 3,
color from Color sensor in input 1, and proximity from IR sensor on input 2. It
returns None
when the button is released, and Some(color, proximity)
when the button
is pushed.
Sensor Computation Expression
With F# 5.0 we can replace these operators by a computation expression.
We create a builder type as usual but implement BindReturn
as map
(with arguments swapped),
and MergeSources
as zip
.
We can also provide a Bind2Return
for the 2 parameter cases as map2
.
The Return
is implemented using ret
, the pure value sensor:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: |
|
Let's rewrite colorAndProximityIfPushed
sensor with this computation expression:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: |
|
Notice the usage of and! to combine all the sensors together.
We put it all together in a final sample:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: |
|
Take your time to extend it to send any command and combine any sensors in a friendly and readable way.
The applicative computation expression for sensors provide a safe API over an error prone
low level protocol. The syntax of computation expresions is also less obscure to
newcomers than the <!>
and <*>
operators that can be a bit hard to explain.
Now you know what to ask Santa for Xmas ! Happy Xmas !
val uint16 : value:'T -> uint16 (requires member op_Explicit)
--------------------
type uint16 = System.UInt16
| Request of sequence: Sequence * (ReadOnlyMemory<byte> -> unit)
| Forward of sequence: Sequence * ReadOnlyMemory<byte>
type ReadOnlyMemory<'T> =
struct
new : array:'T[] -> ReadOnlyMemory<'T> + 1 overload
member CopyTo : destination:Memory<'T> -> unit
member Equals : obj:obj -> bool + 1 overload
member GetHashCode : unit -> int
member IsEmpty : bool
member Length : int
member Pin : unit -> MemoryHandle
member Slice : start:int -> ReadOnlyMemory<'T> + 1 overload
member Span : ReadOnlySpan<'T>
member ToArray : unit -> 'T[]
...
end
--------------------
ReadOnlyMemory ()
ReadOnlyMemory(array: 'T []) : ReadOnlyMemory<'T>
ReadOnlyMemory(array: 'T [], start: int, length: int) : ReadOnlyMemory<'T>
val byte : value:'T -> byte (requires member op_Explicit)
--------------------
type byte = Byte
type MailboxProcessor<'Msg> =
interface IDisposable
new : body:(MailboxProcessor<'Msg> -> Async<unit>) * ?cancellationToken:CancellationToken -> MailboxProcessor<'Msg>
member Post : message:'Msg -> unit
member PostAndAsyncReply : buildMessage:(AsyncReplyChannel<'Reply> -> 'Msg) * ?timeout:int -> Async<'Reply>
member PostAndReply : buildMessage:(AsyncReplyChannel<'Reply> -> 'Msg) * ?timeout:int -> 'Reply
member PostAndTryAsyncReply : buildMessage:(AsyncReplyChannel<'Reply> -> 'Msg) * ?timeout:int -> Async<'Reply option>
member Receive : ?timeout:int -> Async<'Msg>
member Scan : scanner:('Msg -> Async<'T> option) * ?timeout:int -> Async<'T>
member Start : unit -> unit
member TryPostAndReply : buildMessage:(AsyncReplyChannel<'Reply> -> 'Msg) * ?timeout:int -> 'Reply option
...
--------------------
new : body:(MailboxProcessor<'Msg> -> Async<unit>) * ?cancellationToken:CancellationToken -> MailboxProcessor<'Msg>
module Map
from Microsoft.FSharp.Collections
--------------------
type Map<'Key,'Value (requires comparison)> =
interface IReadOnlyDictionary<'Key,'Value>
interface IReadOnlyCollection<KeyValuePair<'Key,'Value>>
interface IEnumerable
interface IComparable
interface IEnumerable<KeyValuePair<'Key,'Value>>
interface ICollection<KeyValuePair<'Key,'Value>>
interface IDictionary<'Key,'Value>
new : elements:seq<'Key * 'Value> -> Map<'Key,'Value>
member Add : key:'Key * value:'Value -> Map<'Key,'Value>
member ContainsKey : key:'Key -> bool
...
--------------------
new : elements:seq<'Key * 'Value> -> Map<'Key,'Value>
type Brick =
interface IDisposable
new : name:string -> Brick
member AsyncRequest : sequence:Sequence * data:ReadOnlyMemory<byte> -> Async<ReadOnlyMemory<byte>>
member AsyncWrite : data:ReadOnlyMemory<byte> -> Async<unit>
member Connect : unit -> unit
member GetNextSequence : unit -> uint16
--------------------
new : name:string -> Brick
type SerialPort =
inherit Component
new : unit -> SerialPort + 6 overloads
member BaseStream : Stream
member BaudRate : int with get, set
member BreakState : bool with get, set
member BytesToRead : int
member BytesToWrite : int
member CDHolding : bool
member Close : unit -> unit
member CtsHolding : bool
member DataBits : int with get, set
...
--------------------
IO.Ports.SerialPort() : IO.Ports.SerialPort
IO.Ports.SerialPort(container: ComponentModel.IContainer) : IO.Ports.SerialPort
IO.Ports.SerialPort(portName: string) : IO.Ports.SerialPort
IO.Ports.SerialPort(portName: string, baudRate: int) : IO.Ports.SerialPort
IO.Ports.SerialPort(portName: string, baudRate: int, parity: IO.Ports.Parity) : IO.Ports.SerialPort
IO.Ports.SerialPort(portName: string, baudRate: int, parity: IO.Ports.Parity, dataBits: int) : IO.Ports.SerialPort
IO.Ports.SerialPort(portName: string, baudRate: int, parity: IO.Ports.Parity, dataBits: int, stopBits: IO.Ports.StopBits) : IO.Ports.SerialPort
type BinaryReader =
new : input:Stream -> BinaryReader + 2 overloads
member BaseStream : Stream
member Close : unit -> unit
member Dispose : unit -> unit
member PeekChar : unit -> int
member Read : unit -> int + 4 overloads
member ReadBoolean : unit -> bool
member ReadByte : unit -> byte
member ReadBytes : count:int -> byte[]
member ReadChar : unit -> char
...
--------------------
IO.BinaryReader(input: IO.Stream) : IO.BinaryReader
IO.BinaryReader(input: IO.Stream, encoding: Encoding) : IO.BinaryReader
IO.BinaryReader(input: IO.Stream, encoding: Encoding, leaveOpen: bool) : IO.BinaryReader
module Event
from Microsoft.FSharp.Control
--------------------
type Event<'T> =
new : unit -> Event<'T>
member Trigger : arg:'T -> unit
member Publish : IEvent<'T>
--------------------
type Event<'Delegate,'Args (requires delegate and 'Delegate :> Delegate)> =
new : unit -> Event<'Delegate,'Args>
member Trigger : sender:obj * args:'Args -> unit
member Publish : IEvent<'Delegate,'Args>
--------------------
new : unit -> Event<'T>
--------------------
new : unit -> Event<'Delegate,'Args>
| Chars = 1
| Eof = 2
val int : value:'T -> int (requires member op_Explicit)
--------------------
type int = int32
--------------------
type int<'Measure> = int
static val IsLittleEndian : bool
static member DoubleToInt64Bits : value:float -> int64
static member GetBytes : value:bool -> byte[] + 9 overloads
static member Int32BitsToSingle : value:int -> float32
static member Int64BitsToDouble : value:int64 -> float
static member SingleToInt32Bits : value:float32 -> int
static member ToBoolean : value:ReadOnlySpan<byte> -> bool + 1 overload
static member ToChar : value:ReadOnlySpan<byte> -> char + 1 overload
static member ToDouble : value:ReadOnlySpan<byte> -> float + 1 overload
static member ToInt16 : value:ReadOnlySpan<byte> -> int16 + 1 overload
...
BitConverter.ToUInt16(value: byte [], startIndex: int) : uint16
static member Add : location1:int * value:int -> int + 1 overload
static member CompareExchange : location1:int * value:int * comparand:int -> int + 6 overloads
static member Decrement : location:int -> int + 1 overload
static member Exchange : location1:int * value:int -> int + 6 overloads
static member Increment : location:int -> int + 1 overload
static member MemoryBarrier : unit -> unit
static member MemoryBarrierProcessWide : unit -> unit
static member Read : location:int64 -> int64
Interlocked.Increment(location: byref<int>) : int
val uint16 : value:'T -> uint16 (requires member op_Explicit)
--------------------
type uint16 = UInt16
IO.Stream.WriteAsync(buffer: byte [], offset: int, count: int) : Tasks.Task
IO.Stream.WriteAsync(buffer: byte [], offset: int, count: int, cancellationToken: CancellationToken) : Tasks.Task
type Async =
static member AsBeginEnd : computation:('Arg -> Async<'T>) -> ('Arg * AsyncCallback * obj -> IAsyncResult) * (IAsyncResult -> 'T) * (IAsyncResult -> unit)
static member AwaitEvent : event:IEvent<'Del,'T> * ?cancelAction:(unit -> unit) -> Async<'T> (requires delegate and 'Del :> Delegate)
static member AwaitIAsyncResult : iar:IAsyncResult * ?millisecondsTimeout:int -> Async<bool>
static member AwaitTask : task:Task -> Async<unit>
static member AwaitTask : task:Task<'T> -> Async<'T>
static member AwaitWaitHandle : waitHandle:WaitHandle * ?millisecondsTimeout:int -> Async<bool>
static member CancelDefaultToken : unit -> unit
static member Catch : computation:Async<'T> -> Async<Choice<'T,exn>>
static member Choice : computations:seq<Async<'T option>> -> Async<'T option>
static member FromBeginEnd : beginAction:(AsyncCallback * obj -> IAsyncResult) * endAction:(IAsyncResult -> 'T) * ?cancelAction:(unit -> unit) -> Async<'T>
...
--------------------
type Async<'T> =
static member Async.AwaitTask : task:Tasks.Task<'T> -> Async<'T>
member Dispose : unit -> unit
val byte : value:'T -> byte (requires member op_Explicit)
--------------------
type byte = System.Byte
| UIRead_GetFirmware = 33034
| UIWrite_LED = 33307
| UIButton_Pressed = 33545
| UIDraw_Update = 33792
| UIDraw_Clean = 33793
| UIDraw_Pixel = 33794
| UIDraw_Line = 33795
| UIDraw_Circle = 33796
| UIDraw_Text = 33797
| UIDraw_FillRect = 33801
| UIDraw_Rect = 33802
| UIDraw_InverseRect = 33808
| UIDraw_SelectFont = 33809
| UIDraw_Topline = 33810
| UIDraw_FillWindow = 33811
| UIDraw_DotLine = 33813
| UIDraw_FillCircle = 33816
| UIDraw_BmpFile = 33820
| Sound_Break = 37888
| Sound_Tone = 37889
| Sound_Play = 37890
| Sound_Repeat = 37891
| Sound_Service = 37892
| InputDevice_GetTypeMode = 39173
| InputDevice_GetDeviceName = 39189
| InputDevice_GetModeName = 39190
| InputDevice_ReadyPct = 39195
| InputDevice_ReadyRaw = 39196
| InputDevice_ReadySI = 39197
| InputDevice_ClearAll = 39178
| InputDevice_ClearChanges = 39194
| InputRead = 154
| InputReadExt = 158
| InputReadSI = 157
| OutputStop = 163
| OutputPower = 164
| OutputSpeed = 165
| OutputStart = 166
| OutputPolarity = 167
| OutputReady = 170
| OutputStepPower = 172
| OutputTimePower = 173
| OutputStepSpeed = 174
| OutputTimeSpeed = 175
| OutputStepSync = 176
| OutputTimeSync = 177
| BeginDownload = 146
| ContinueDownload = 147
| CloseFileHandle = 152
| CreateDirectory = 155
| DeleteFile = 156
static member BlockCopy : src:Array * srcOffset:int * dst:Array * dstOffset:int * count:int -> unit
static member ByteLength : array:Array -> int
static member GetByte : array:Array * index:int -> byte
static member MemoryCopy : source:unit * destination:unit * destinationSizeInBytes:int64 * sourceBytesToCopy:int64 -> unit + 1 overload
static member SetByte : array:Array * index:int * value:byte -> unit
Span.Slice(start: int, length: int) : Span<byte>
static member ReadInt16BigEndian : source:ReadOnlySpan<byte> -> int16
static member ReadInt16LittleEndian : source:ReadOnlySpan<byte> -> int16
static member ReadInt32BigEndian : source:ReadOnlySpan<byte> -> int
static member ReadInt32LittleEndian : source:ReadOnlySpan<byte> -> int
static member ReadInt64BigEndian : source:ReadOnlySpan<byte> -> int64
static member ReadInt64LittleEndian : source:ReadOnlySpan<byte> -> int64
static member ReadUInt16BigEndian : source:ReadOnlySpan<byte> -> uint16
static member ReadUInt16LittleEndian : source:ReadOnlySpan<byte> -> uint16
static member ReadUInt32BigEndian : source:ReadOnlySpan<byte> -> uint32
static member ReadUInt32LittleEndian : source:ReadOnlySpan<byte> -> uint32
...
val uint32 : value:'T -> uint32 (requires member op_Explicit)
--------------------
type uint32 = UInt32
val string : value:'T -> string
--------------------
type string = String
member BodyName : string
member Clone : unit -> obj
member CodePage : int
member DecoderFallback : DecoderFallback with get, set
member EncoderFallback : EncoderFallback with get, set
member EncodingName : string
member Equals : value:obj -> bool
member GetByteCount : chars:char[] -> int + 5 overloads
member GetBytes : chars:char[] -> byte[] + 7 overloads
member GetCharCount : bytes:byte[] -> int + 3 overloads
...
Encoding.GetBytes(chars: char []) : byte []
Encoding.GetBytes(chars: ReadOnlySpan<char>, bytes: Span<byte>) : int
Encoding.GetBytes(s: string, index: int, count: int) : byte []
Encoding.GetBytes(chars: char [], index: int, count: int) : byte []
Encoding.GetBytes(chars: nativeptr<char>, charCount: int, bytes: nativeptr<byte>, byteCount: int) : int
Encoding.GetBytes(s: string, charIndex: int, charCount: int, bytes: byte [], byteIndex: int) : int
Encoding.GetBytes(chars: char [], charIndex: int, charCount: int, bytes: byte [], byteIndex: int) : int
(extension) String.AsSpan(start: int) : ReadOnlySpan<char>
(extension) String.AsSpan(start: int, length: int) : ReadOnlySpan<char>
| Byte of uint8
| UShort of uint16
| UInt of uint32
| String of string
| GlobalIndex of uint8
union case Parameter.Byte: uint8 -> Parameter
--------------------
type Byte =
struct
member CompareTo : value:obj -> int + 1 overload
member Equals : obj:obj -> bool + 1 overload
member GetHashCode : unit -> int
member GetTypeCode : unit -> TypeCode
member ToString : unit -> string + 3 overloads
member TryFormat : destination:Span<char> * charsWritten:int * ?format:ReadOnlySpan<char> * ?provider:IFormatProvider -> bool
static val MaxValue : byte
static val MinValue : byte
static member Parse : s:string -> byte + 4 overloads
static member TryParse : s:string * result:byte -> bool + 3 overloads
end
val uint8 : value:'T -> uint8 (requires member op_Explicit)
--------------------
type uint8 = Byte
union case Parameter.String: string -> Parameter
--------------------
type String =
new : value:char[] -> string + 8 overloads
member Chars : int -> char
member Clone : unit -> obj
member CompareTo : value:obj -> int + 1 overload
member Contains : value:string -> bool + 3 overloads
member CopyTo : sourceIndex:int * destination:char[] * destinationIndex:int * count:int -> unit
member EndsWith : value:string -> bool + 3 overloads
member EnumerateRunes : unit -> StringRuneEnumerator
member Equals : obj:obj -> bool + 2 overloads
member GetEnumerator : unit -> CharEnumerator
...
--------------------
String(value: char []) : String
String(value: nativeptr<char>) : String
String(value: nativeptr<sbyte>) : String
String(value: ReadOnlySpan<char>) : String
String(c: char, count: int) : String
String(value: char [], startIndex: int, length: int) : String
String(value: nativeptr<char>, startIndex: int, length: int) : String
String(value: nativeptr<sbyte>, startIndex: int, length: int) : String
String(value: nativeptr<sbyte>, startIndex: int, length: int, enc: Encoding) : String
val byte : byte
--------------------
type byte = Byte
val int : byte
--------------------
type int = int32
--------------------
type int<'Measure> = int
val string : byte
--------------------
type string = String
from 2020-12-03-applicative-computation-expressions-3
Encoding.GetByteCount(s: string) : int
Encoding.GetByteCount(chars: char []) : int
Encoding.GetByteCount(chars: nativeptr<char>, count: int) : int
Encoding.GetByteCount(s: string, index: int, count: int) : int
Encoding.GetByteCount(chars: char [], index: int, count: int) : int
module List
from Microsoft.FSharp.Collections
--------------------
type List<'T> =
| ( [] )
| ( :: ) of Head: 'T * Tail: 'T list
interface IReadOnlyList<'T>
interface IReadOnlyCollection<'T>
interface IEnumerable
interface IEnumerable<'T>
member GetReverseIndex : rank:int * offset:int -> int
member GetSlice : startIndex:int option * endIndex:int option -> 'T list
member Head : 'T
member IsEmpty : bool
member Item : index:int -> 'T with get
member Length : int
...
delegate of 't * Span<byte> -> Span<byte>
member Dispose : unit -> unit
member MaxBufferSize : int
member Rent : ?minBufferSize:int -> IMemoryOwner<'T>
static member Shared : MemoryPool<'T>
Memory.Slice(start: int, length: int) : Memory<byte>
inherit IDisposable
member Memory : Memory<'T>
type Memory<'T> =
struct
new : array:'T[] -> Memory<'T> + 1 overload
member CopyTo : destination:Memory<'T> -> unit
member Equals : obj:obj -> bool + 1 overload
member GetHashCode : unit -> int
member IsEmpty : bool
member Length : int
member Pin : unit -> MemoryHandle
member Slice : start:int -> Memory<'T> + 1 overload
member Span : Span<'T>
member ToArray : unit -> 'T[]
...
end
--------------------
Memory ()
Memory(array: 'T []) : Memory<'T>
Memory(array: 'T [], start: int, length: int) : Memory<'T>
from 2020-12-03-applicative-computation-expressions-3
Memory.op_Implicit(segment: ArraySegment<'T>) : Memory<'T>
Memory.op_Implicit(array: 'T []) : Memory<'T>
| A
| B
| C
| D
union case Brake.Brake: Brake
--------------------
type Brake =
| Brake
| NoBrake
static member toByte : b:Brake -> Parameter
union case Power.Power: uint8 -> Power
--------------------
type Power = | Power of uint8
wait for the end of prvious command
type LegoBuilder =
new : unit -> LegoBuilder
member Bind : x:Lego<'a> * f:('a -> Lego<'b>) -> Lego<'b>
member Bind : command:Command list * f:(unit -> Lego<'b>) -> Lego<'b>
member Bind : command:Command * f:(unit -> Lego<'b>) -> Lego<'b>
member Bind : x:Async<'a> * f:('a -> Lego<'b>) -> Lego<'b>
member Combine : x:(Brick -> Async<unit>) * y:(Brick -> Async<'a>) -> Lego<'a>
member Delay : f:(unit -> Lego<'a>) -> (Brick -> Async<'a>)
member For : values:seq<'T> * body:('T -> Lego<unit>) -> (Brick -> Async<unit>)
member Return : x:'a -> Lego<'a>
member ReturnFrom : x:'e -> 'e
...
--------------------
new : unit -> LegoBuilder
val seq : sequence:seq<'T> -> seq<'T>
--------------------
type seq<'T> = Collections.Generic.IEnumerable<'T>
type CancellationTokenSource =
new : unit -> CancellationTokenSource + 2 overloads
member Cancel : unit -> unit + 1 overload
member CancelAfter : delay:TimeSpan -> unit + 1 overload
member Dispose : unit -> unit
member IsCancellationRequested : bool
member Token : CancellationToken
static member CreateLinkedTokenSource : [<ParamArray>] tokens:CancellationToken[] -> CancellationTokenSource + 1 overload
--------------------
CancellationTokenSource() : CancellationTokenSource
CancellationTokenSource(delay: TimeSpan) : CancellationTokenSource
CancellationTokenSource(millisecondsDelay: int) : CancellationTokenSource
| In1
| In2
| In3
| In4
| InA
| InB
| InC
| InD
gets the binary code for an input port
| TouchMode of TouchMode
| ColorMode of ColorMode
| IRMode of IRMode
union case Mode.TouchMode: TouchMode -> Mode
--------------------
type TouchMode =
| Touch
| Bumps
union case Mode.ColorMode: ColorMode -> Mode
--------------------
type ColorMode =
| Reflective
| Ambient
| Color
| ReflectiveRaw
| ReflectiveRgb
| Calibration
union case Mode.IRMode: IRMode -> Mode
--------------------
type IRMode =
| Proximity
| Seek
| Remote
| RemoteA
| SAlt
| Calibrate
| Touch
| Bumps
| Reflective
| Ambient
| Color
| ReflectiveRaw
| ReflectiveRgb
| Calibration
| Proximity
| Seek
| Remote
| RemoteA
| SAlt
| Calibrate
| SI
| Raw
| Percent
| RGB
gets the byte length for each data type
| DirectReply = 2
| SystemReply = 3
| DirectReplyError = 4
| SystemReplyError = 5
ReadOnlyMemory.Slice(start: int, length: int) : ReadOnlyMemory<byte>
{ Inputs: Set<InputRequest>
Get: Map<InputRequest,ReadOnlyMemory<byte>> -> 't }
module Set
from Microsoft.FSharp.Collections
--------------------
type Set<'T (requires comparison)> =
interface IReadOnlyCollection<'T>
interface IComparable
interface IEnumerable
interface IEnumerable<'T>
interface ICollection<'T>
new : elements:seq<'T> -> Set<'T>
member Add : value:'T -> Set<'T>
member Contains : value:'T -> bool
override Equals : obj -> bool
member IsProperSubsetOf : otherSet:Set<'T> -> bool
...
--------------------
new : elements:seq<'T> -> Set<'T>
BitConverter.ToSingle(value: byte [], startIndex: int) : float32
BitConverter.ToInt32(value: byte [], startIndex: int) : int
ReadOnlySpan.Slice(start: int, length: int) : ReadOnlySpan<byte>
| Pushed
| Released
union case ColorMode.Color: ColorMode
--------------------
type Color =
| Transparent
| Black
| Blue
| Green
| Yellow
| Red
| White
| Brown
union case ReadDataType.Raw: ReadDataType
--------------------
module Raw
from 2020-12-03-applicative-computation-expressions-3.Sensors.Color
union case TouchMode.Touch: TouchMode
--------------------
module Touch
from 2020-12-03-applicative-computation-expressions-3.Sensors
from 2020-12-03-applicative-computation-expressions-3
from 2020-12-03-applicative-computation-expressions-3.Sensors
from 2020-12-03-applicative-computation-expressions-3.Sensors
from 2020-12-03-applicative-computation-expressions-3.Sensors
type SensorBuilder =
new : unit -> SensorBuilder
member Bind2Return : x:Sensor<'a> * y:Sensor<'b> * f:('a * 'b -> 'c) -> Sensor<'c>
member BindReturn : x:Sensor<'a> * f:('a -> 'b) -> Sensor<'b>
member MergeSources : x:Sensor<'a> * y:Sensor<'b> -> Sensor<'a * 'b>
member Return : x:'a -> Sensor<'a>
--------------------
new : unit -> SensorBuilder
from 2020-12-03-applicative-computation-expressions-3.Sensors.Color