Gjallarhorn


Signals in Gjallarhorn

One of the core types in Gjallarhorn is a Signal.

A signal represents a value that changes over time, and has the following properties: - A current value, which can always be fetched - A mechanism to signal changes to subscribers that the value has been updated

You can think if a signal as a window into data that changes over time. It's similar to an observable, and even implements IObservable for compatibility with other libraries, except that it always has a current value.

The simplest signal can be created via Signal.constant:

1: 
2: 
3: 
4: 
5: 
6: 
7: 
8: 
9: 
#r "Gjallarhorn.dll"
open Gjallarhorn
open System

// Create a signal over a constant value
let s = Signal.constant 42

// Prints "Value = 42"
printfn "Value = %d" s.Value

As it's name suggests, Signal.constant takes a constant value and binds it into a signal, which is represented via the ISignal<'a> interface.

ISignal<'a> provides a simple .Value property, which we can use to fetch the current value at any point.

All mutables in Gjallarhorn also implement ISignal<'a>, allowing mutables to be used as signals as well. The Signal module provides functionality which allows you to subscribe to changes on signals:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
// Create a mutable variable
let m = Mutable.create 0

Signal.Subscription.create (fun currentValue -> printfn "Value is now %d" currentValue) m 

// After this is set, a print will occur
// Prints: "Value is now 1"
m.Value <- 1

// After this is set, a second print will occur with the new value
// Prints: "Value is now 2"
m.Value <- 2

Signals can also be filtered or transformed:

 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: 
// Create a mutable value
let source = Mutable.create 0

// Create a filter on this
let filtered = Signal.filter (fun s -> s < 5) source

// Transform the filtered result:
let final = Signal.map (fun s -> sprintf "Filtered value is %d" s) filtered

// Subscribe to the final notifications:
Signal.Subscription.create (fun s -> printfn "%s" s) final

// Now, let's set some values:
// Prints: "Filtered value is 1"
source.Value <- 1

// Prints: "Filtered value is 3"
source.Value <- 3

// Does not output anything, as the filter's predicate fails
source.Value <- 42

// Note that the filtered value retains its "last good value"
// Prints: "Source = 42, filtered = 3"
printfn "Source = %d, filtered = %d" source.Value filtered.Value

// Prints: "Filtered value is 2"
source.Value <- 2

Signals can be combined by using Signal.map2, and even higher arity mapping functions:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
20: 
21: 
22: 
23: 
24: 
let a = Mutable.create ""
let b = Mutable.create ""

let v1 = Signal.map2 (fun v1 v2 -> v1 + v2) a b 

a.Value <- "Foo"
b.Value <- "Bar"

// Prints: "v1.Value = FooBar"
printfn "v1.Value = %s" v1.Value

// Mapping also works to combine many signals at once, including mixed types, up to 10 in an signal function
let c = Mutable.create ""
let d = Mutable.create 0

let v2 = Signal.map4 (fun v1 v2 v3 v4 -> sprintf "%s%s%s : %d" v1 v2 v3 v4) a b c d

// Prints: "v2.Value = FooBar : 0"
printfn "v2.Value = %s" v2.Value

c.Value <- "Baz"
d.Value <- 42
// Prints: "v2.Value = FooBarBaz : 42"
printfn "v2.Value = %s" v2.Value

Signals are also closely related to observables. The main difference between an ISignal<'a> and an IObservable<'a> is that the former has the notion of a value (represented in the Value property) which always exists and is current.

As these are so closely related, ISignal<'a> directly implements IObservable<'a>, and there is a function to convert from IObservable<'a> included in the Signal module.

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
20: 
21: 
22: 
23: 
let e = Event<int>()
let observable = e.Publish

// Subscribe to the observable, with "0" as the initial value (since we always need a "current value")
let s = Signal.fromObservable 0 observable

// Prints: "Signal's value = 0"
printfn "Signal's value = %d" s.Value

e.Trigger 42

// Prints: "Signal's value = 42"
printfn "Signal's value = %d" s.Value

// Convert back to an observable
let obs = s :> IObservable<int>

obs
|> Observable.add (fun s -> printfn "New value of observable = %d" s)

// Prints: "New value of observable = 54"
// Note that this starts as an observable, maps through a Signal, and back to an observable for the notification!
e.Trigger 54

It is also possible to use signals by closing over them with functions. This provides a clean mechanism for working with configuration.

For example:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
type Configuration = { Author : string ; AutoPublish : bool }
let config = Mutable.create { Author = "Foo" ; AutoPublish = false }

let workWithConfig (author: string) op =
    printfn "%s Author = %s" op author

let operation = 
    config
    |> Signal.map (fun c -> c.Author)
    |> Signal.mapFunction workWithConfig

operation "One"
// prints "One Author = Foo"

// Change our configuration
config.Value <- { Author = "Reed" ; AutoPublish = true }

operation "One"
// prints "One Author = Reed"

Now, let's move on to Mutables in Gjallarhorn.

namespace System
val s : obj

Full name: Signals.s
val printfn : format:Printf.TextWriterFormat<'T> -> 'T

Full name: Microsoft.FSharp.Core.ExtraTopLevelOperators.printfn
val m : obj

Full name: Signals.m
val source : obj

Full name: Signals.source
val filtered : obj

Full name: Signals.filtered
val final : obj

Full name: Signals.final
val sprintf : format:Printf.StringFormat<'T> -> 'T

Full name: Microsoft.FSharp.Core.ExtraTopLevelOperators.sprintf
val a : obj

Full name: Signals.a
val b : obj

Full name: Signals.b
val v1 : obj

Full name: Signals.v1
val c : obj

Full name: Signals.c
val d : obj

Full name: Signals.d
val v2 : obj

Full name: Signals.v2
val e : Event<int>

Full name: Signals.e
Multiple items
module Event

from Microsoft.FSharp.Control

--------------------
type Event<'T> =
  new : unit -> Event<'T>
  member Trigger : arg:'T -> unit
  member Publish : IEvent<'T>

Full name: Microsoft.FSharp.Control.Event<_>

--------------------
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>

Full name: Microsoft.FSharp.Control.Event<_,_>

--------------------
new : unit -> Event<'T>

--------------------
new : unit -> Event<'Delegate,'Args>
Multiple items
val int : value:'T -> int (requires member op_Explicit)

Full name: Microsoft.FSharp.Core.Operators.int

--------------------
type int = int32

Full name: Microsoft.FSharp.Core.int

--------------------
type int<'Measure> = int

Full name: Microsoft.FSharp.Core.int<_>
val observable : IEvent<int>

Full name: Signals.observable
property Event.Publish: IEvent<int>
val s : IObservable<int>

Full name: Signals.s
member Event.Trigger : arg:'T -> unit
val obs : IObservable<int>

Full name: Signals.obs
type IObservable<'T> =
  member Subscribe : observer:IObserver<'T> -> IDisposable

Full name: System.IObservable<_>
module Observable

from Microsoft.FSharp.Control
val add : callback:('T -> unit) -> source:IObservable<'T> -> unit

Full name: Microsoft.FSharp.Control.Observable.add
val s : int
Multiple items
namespace System.Configuration

--------------------
type Configuration =
  {Author: string;
   AutoPublish: bool;}

Full name: Signals.Configuration
Configuration.Author: string
Multiple items
val string : value:'T -> string

Full name: Microsoft.FSharp.Core.Operators.string

--------------------
type string = String

Full name: Microsoft.FSharp.Core.string
Configuration.AutoPublish: bool
type bool = Boolean

Full name: Microsoft.FSharp.Core.bool
val config : obj

Full name: Signals.config
val workWithConfig : author:string -> op:string -> unit

Full name: Signals.workWithConfig
val author : string
val op : string
val operation : (string -> obj)

Full name: Signals.operation
Fork me on GitHub