F Sharp Programming/Events
F# : Events |
Events allow objects to communicate with one another through a kind of synchronous message passing. Events are simply hooks to other functions: objects register callback functions to an event, and these callbacks will be executed when (and if) the event is triggered by some object.
For example, let's say we have a clickable button which exposed an event called Click
. We can register a block of code, something like fun () -> printfn "I've been clicked!"
, to the button's click event. When the click event is triggered, it will execute the block of code we've registered. If we wanted to, we could register an indefinite number of callback functions to the click event—the button doesn't care what code is trigged by the callbacks or how many callback functions are registered to its click event, it blindly executes whatever functions are hooked to its click event.
Event-driven programming is natural in GUI code, as GUIs tend to consist of controls which react and respond to user input. Events are, of course, useful in non-GUI applications as well. For example, if we have an object with mutable properties, we may want to notify another object when those properties change.
Defining Events
[edit | edit source]Events are created and used through F#'s Event class. To create an event, use the Event
constructor as follows:
type Person(name : string) =
let mutable _name = name;
let nameChanged = new Event<string>()
member this.Name
with get() = _name
and set(value) = _name <- value
To allow listeners to hook onto our event, we need to expose the nameChanged
field as a public member using the event's Publish
property:
type Person(name : string) =
let mutable _name = name;
let nameChanged = new Event<unit>() (* creates event *)
member this.NameChanged = nameChanged.Publish (* exposed event handler *)
member this.Name
with get() = _name
and set(value) =
_name <- value
nameChanged.Trigger() (* invokes event handler *)
Now, any object can listen to the changes on the person method. By convention and Microsoft's recommendation, events are usually named Verb
or VerbPhrase
, as well as adding tenses like Verbed
and Verbing
to indicate post- and pre-events.
Adding Callbacks to Event Handlers
[edit | edit source]Its very easy to add callbacks to event handlers. Each event handler has the type IEvent<'T>
which exposes several methods:
val Add : event:('T -> unit) -> unit
- Connect a listener function to the event. The listener will be invoked when the event is fired.
val AddHandler : 'del -> unit
- Connect a handler delegate object to the event. A handler can be later removed using RemoveHandler. The listener will be invoked when the event is fired.
val RemoveHandler : 'del -> unit
- Remove a listener delegate from an event listener store.
Here's an example program:
type Person(name : string) =
let mutable _name = name;
let nameChanged = new Event<unit>() (* creates event *)
member this.NameChanged = nameChanged.Publish (* exposed event handler *)
member this.Name
with get() = _name
and set(value) =
_name <- value
nameChanged.Trigger() (* invokes event handler *)
let p = new Person("Bob")
p.NameChanged.Add(fun () -> printfn "-- Name changed! New name: %s" p.Name)
printfn "Event handling is easy"
p.Name <- "Joe"
printfn "It handily decouples objects from one another"
p.Name <- "Moe"
p.NameChanged.Add(fun () -> printfn "-- Another handler attached to NameChanged!")
printfn "It's also causes programs behave non-deterministically."
p.Name <- "Bo"
printfn "The function NameChanged is invoked effortlessly."
This program outputs the following:
Event handling is easy -- Name changed! New name: Joe It handily decouples objects from one another -- Name changed! New name: Moe It's also causes programs behave non-deterministically. -- Name changed! New name: Bo -- Another handler attached to NameChanged! The function NameChanged is invoked effortlessly.
- Note: When multiple callbacks are connected to a single event, they are executed in the order they are added. However, in practice, you should not write code with the expectation that events will trigger in a particular order, as doing so can introduce complex dependencies between functions. Event-driven programming is often non-deterministic and fundamentally stateful, which can occasionally be at odds with the spirit of functional programming. Its best to write callback functions which do not modify state, and do not depend on the invocation of any prior events.
Working with EventHandlers Explicitly
[edit | edit source]Adding and Removing Event Handlers
[edit | edit source]The code above demonstrates how to use the IEvent<'T>.add
method. However, occasionally we need to remove callbacks. To do so, we need to work with the IEvent<'T>.AddHandler
and IEvent<'T>.RemoveHandler
methods, as well as .NET's built-in System.Delegate type.
The function person.NameChanged.AddHandler
has the type val AddHandler : Handler<'T> -> unit
, where Handler<'T>
inherits from System.Delegate
. We can create an instance of Handler
as follows:
type Person(name : string) =
let mutable _name = name;
let nameChanged = new Event<unit>() (* creates event *)
member this.NameChanged = nameChanged.Publish (* exposed event handler *)
member this.Name
with get() = _name
and set(value) =
_name <- value
nameChanged.Trigger() (* invokes event handler *)
let p = new Person("Bob")
let person_NameChanged =
new Handler<unit>(fun sender eventargs -> printfn "-- Name changed! New name: %s" p.Name)
p.NameChanged.AddHandler(person_NameChanged)
printfn "Event handling is easy"
p.Name <- "Joe"
printfn "It handily decouples objects from one another"
p.Name <- "Moe"
p.NameChanged.RemoveHandler(person_NameChanged)
p.NameChanged.Add(fun () -> printfn "-- Another handler attached to NameChanged!")
printfn "It's also causes programs behave non-deterministically."
p.Name <- "Bo"
printfn "The function NameChanged is invoked effortlessly."
This program outputs the following:
Event handling is easy -- Name changed! New name: Joe It handily decouples objects from one another -- Name changed! New name: Moe It's also causes programs behave non-deterministically. -- Another handler attached to NameChanged! The function NameChanged is invoked effortlessly.
Defining New Delegate Types
[edit | edit source]F#'s event handling model is a little different from the rest of .NET. If we want to expose F# events to different languages like C# or VB.NET, we can define a custom delegate type which compiles to a .NET delegate using the delegate
keyword, for example:
type NameChangingEventArgs(oldName : string, newName : string) =
inherit System.EventArgs()
member this.OldName = oldName
member this.NewName = newName
type NameChangingDelegate = delegate of obj * NameChangingEventArgs -> unit
The convention obj * NameChangingEventArgs
corresponds to the .NET naming guidelines which recommend that all events have the type val eventName : (sender : obj * e : #EventArgs) -> unit
.
Use existing .NET WPF Event and Delegate Types
[edit | edit source]Try using existing .NET WPF Event and Delegate, example, ClickEvent and RoutedEventHandler. Create F# Windows Application .NET project with referring these libraries (PresentationCore PresentationFramework System.Xaml WindowsBase). The program will display a button in a window. Clicking the button will display the button's content as string.
open System.Windows
open System.Windows.Controls
open System.Windows.Input
open System
[<EntryPoint>] [<STAThread>] // STAThread is Single-Threading-Apartment which is required by WPF
let main argv =
let b = new Button(Content="Button") // b is a Button with "Button" as content
let f(sender:obj)(e:RoutedEventArgs) = // (#3) f is a fun going to handle the Button.ClickEvent
// f signature must be curried, not tuple as governed by Delegate-RoutedEventHandler.
// that means f(sender:obj,e:RoutedEventArgs) will not work.
let b = sender:?>Button // sender will have Button-type. Convert it to Button into b.
MessageBox.Show(b.Content:?>string) // Retrieve the content of b which is obj.
// Convert it to string and display by <code>Messagebox.Show</code>
|> ignore // ignore the return because f-signature requires: obj->RoutedEventArgs->unit
let d = new RoutedEventHandler(f) // (#2) d will have type-RoutedEventHandler,
// RoutedEventHandler is a kind of delegate to handle Button.ClickEvent.
// The f must have signature governed by RoutedEventHandler.
b.AddHandler(Button.ClickEvent,d) // (#1) attach a RountedEventHandler-d for Button.ClickEvent
let w = new Window(Visibility=Visibility.Visible,Content=b) // create a window-w have a Button-b
// which will show the content of b when clicked
(new Application()).Run(w) // create new Application() running the Window-w.
b.AddHandler(Button.ClickEvent,d)
let d = new RoutedEventHandler(f)
let f(sender:obj)(e:RoutedEventArgs) = ....
void RoutedEventHandler(object sender, RoutedEventArgs e)
. So f must have same signature. Present the signature in F# is (obj * RountedEventHandler) -> unit
Passing State To Callbacks
[edit | edit source]Events can pass state to callbacks with minimal effort. Here is a simple program which reads a file in blocks of characters:
open System
type SuperFileReader() =
let progressChanged = new Event<int>()
member this.ProgressChanged = progressChanged.Publish
member this.OpenFile (filename : string, charsPerBlock) =
use sr = new System.IO.StreamReader(filename)
let streamLength = int64 sr.BaseStream.Length
let sb = new System.Text.StringBuilder(int streamLength)
let charBuffer = Array.zeroCreate<char> charsPerBlock
let mutable oldProgress = 0
let mutable totalCharsRead = 0
progressChanged.Trigger(0)
while not sr.EndOfStream do
(* sr.ReadBlock returns number of characters read from stream *)
let charsRead = sr.ReadBlock(charBuffer, 0, charBuffer.Length)
totalCharsRead <- totalCharsRead + charsRead
(* appending chars read from buffer *)
sb.Append(charBuffer, 0, charsRead) |> ignore
let newProgress = int(decimal totalCharsRead / decimal streamLength * 100m)
if newProgress > oldProgress then
progressChanged.Trigger(newProgress) // passes newProgress as state to callbacks
oldProgress <- newProgress
sb.ToString()
let fileReader = new SuperFileReader()
fileReader.ProgressChanged.Add(fun percent ->
printfn "%i percent done..." percent)
let x = fileReader.OpenFile(@"C:\Test.txt", 50)
printfn "%s[...]" x.[0 .. if x.Length <= 100 then x.Length - 1 else 100]
This program has the following types:
type SuperFileReader =
class
new : unit -> SuperFileReader
member OpenFile : filename:string * charsToRead:int -> string
member ProgressChanged : IEvent<int>
end
val fileReader : SuperFileReader
val x : string
Since our event has the type IEvent<int>
, we can pass int
data as state to listening callbacks. This program outputs the following:
0 percent done... 4 percent done... 9 percent done... 14 percent done... 19 percent done... 24 percent done... 29 percent done... 34 percent done... 39 percent done... 44 percent done... 49 percent done... 53 percent done... 58 percent done... 63 percent done... 68 percent done... 73 percent done... 78 percent done... 83 percent done... 88 percent done... 93 percent done... 98 percent done... 100 percent done... In computer programming, event-driven programming or event-based programming is a programming paradig{{typo help inline|reason=similar to parading|date=September 2022}}[...]
Retrieving State from Callers
[edit | edit source]A common idiom in event-driven programming is pre- and post-event handling, as well as the ability to cancel events. Cancellation requires two-way communication between an event handler and a listener, which we can easily accomplish through the use of ref cells or mutable members:
type Person(name : string) =
let mutable _name = name;
let nameChanging = new Event<string * bool ref>()
let nameChanged = new Event<unit>()
member this.NameChanging = nameChanging.Publish
member this.NameChanged = nameChanged.Publish
member this.Name
with get() = _name
and set(value) =
let cancelChange = ref false
nameChanging.Trigger(value, cancelChange)
if not !cancelChange then
_name <- value
nameChanged.Trigger()
let p = new Person("Bob")
p.NameChanging.Add(fun (name, cancel) ->
let exboyfriends = ["Steve"; "Mike"; "Jon"; "Seth"]
if List.exists (fun forbiddenName -> forbiddenName = name) exboyfriends then
printfn "-- No %s's allowed!" name
cancel := true
else
printfn "-- Name allowed")
p.NameChanged.Add(fun () ->
printfn "-- Name changed to %s" p.Name)
let tryChangeName newName =
printfn "Attempting to change name to '%s'" newName
p.Name <- newName
tryChangeName "Joe"
tryChangeName "Moe"
tryChangeName "Jon"
tryChangeName "Thor"
This program has the following types:
type Person =
class
new : name:string -> Person
member Name : string
member NameChanged : IEvent<unit>
member NameChanging : IEvent<string * bool ref>
member Name : string with set
end
val p : Person
val tryChangeName : string -> unit
This program outputs the following:
Attempting to change name to 'Joe' -- Name allowed -- Name changed to Joe Attempting to change name to 'Moe' -- Name allowed -- Name changed to Moe Attempting to change name to 'Jon' -- No Jon's allowed! Attempting to change name to 'Thor' -- Name allowed -- Name changed to Thor
If we need to pass a significant amount of state to listeners, then its recommended to wrap the state in an object as follows:
type NameChangingEventArgs(newName : string) =
inherit System.EventArgs()
let mutable cancel = false
member this.NewName = newName
member this.Cancel
with get() = cancel
and set(value) = cancel <- value
type Person(name : string) =
let mutable _name = name;
let nameChanging = new Event<NameChangingEventArgs>()
let nameChanged = new Event<unit>()
member this.NameChanging = nameChanging.Publish
member this.NameChanged = nameChanged.Publish
member this.Name
with get() = _name
and set(value) =
let eventArgs = new NameChangingEventArgs(value)
nameChanging.Trigger(eventArgs)
if not eventArgs.Cancel then
_name <- value
nameChanged.Trigger()
let p = new Person("Bob")
p.NameChanging.Add(fun e ->
let exboyfriends = ["Steve"; "Mike"; "Jon"; "Seth"]
if List.exists (fun forbiddenName -> forbiddenName = e.NewName) exboyfriends then
printfn "-- No %s's allowed!" e.NewName
e.Cancel <- true
else
printfn "-- Name allowed")
(* ... rest of program ... *)
By convention, custom event parameters should inherit from System.EventArgs
, and should have the suffix EventArgs
.
Using the Event Module
[edit | edit source]F# allows users to pass event handlers around as first-class values in fundamentally the same way as all other functions. The Event module has a variety of functions for working with event handlers:
val choose : ('T -> 'U option) -> IEvent<'del,'T> -> IEvent<'U> (requires delegate and 'del :> Delegate)
- Return a new event which fires on a selection of messages from the original event. The selection function takes an original message to an optional new message.
val filter : ('T -> bool) -> IEvent<'del,'T> -> IEvent<'T> (requires delegate and 'del :> Delegate)
- Return a new event that listens to the original event and triggers the resulting event only when the argument to the event passes the given function.
val listen : ('T -> unit) -> IEvent<'del,'T> -> unit (requires delegate and 'del :> Delegate)
- Run the given function each time the given event is triggered.
val map : ('T -> 'U) -> IEvent<'del,'T> -> IEvent<'U> (requires delegate and 'del :> Delegate)
- Return a new event which fires on a selection of messages from the original event. The selection function takes an original message to an optional new message.
val merge : IEvent<'del1,'T> -> IEvent<'del2,'T> -> IEvent<'T> (requires delegate and 'del1 :> Delegate and delegate and 'del2 :> Delegate)
- Fire the output event when either of the input events fire.
val pairwise : IEvent<'del,'T> -> IEvent<'T * 'T> (requires delegate and 'del :> Delegate)
- Return a new event that triggers on the second and subsequent triggerings of the input event. The Nth triggering of the input event passes the arguments from the N-1th and Nth triggering as a pair. The argument passed to the N-1th triggering is held in hidden internal state until the Nth triggering occurs. You should ensure that the contents of the values being sent down the event are not mutable. Note that many EventArgs types are mutable, e.g. MouseEventArgs, and each firing of an event using this argument type may reuse the same physical argument object with different values. In this case you should extract the necessary information from the argument before using this combinator.
val partition : ('T -> bool) -> IEvent<'del,'T> -> IEvent<'T> * IEvent<'T> (requires delegate and 'del :> Delegate
- Return a new event that listens to the original event and triggers the first resulting event if the application of the predicate to the event arguments returned true, and the second event if it returned false.
val scan : ('U -> 'T -> 'U) -> 'U -> IEvent<'del,'T> -> IEvent<'U> (requires delegate and 'del :> Delegate)
- Return a new event consisting of the results of applying the given accumulating function to successive values triggered on the input event. An item of internal state records the current value of the state parameter. The internal state is not locked during the execution of the accumulation function, so care should be taken that the input IEvent not triggered by multiple threads simultaneously.
val split : ('T -> Choice<'U1,'U2>) -> IEvent<'del,'T> -> IEvent<'U1> * IEvent<'U2> (requires delegate and 'del :> Delegate)
- Return a new event that listens to the original event and triggers the first resulting event if the application of the function to the event arguments returned a Choice2Of1, and the second event if it returns a Choice2Of2.
Take the following snippet:
p.NameChanging.Add(fun (e : NameChangingEventArgs) ->
let exboyfriends = ["Steve"; "Mike"; "Jon"; "Seth"]
if List.exists (fun forbiddenName -> forbiddenName = e.NewName) exboyfriends then
printfn "-- No %s's allowed!" e.NewName
e.Cancel <- true)
We can rewrite this in a more functional style as follows:
p.NameChanging
|> Event.filter (fun (e : NameChangingEventArgs) ->
let exboyfriends = ["Steve"; "Mike"; "Jon"; "Seth"]
List.exists (fun forbiddenName -> forbiddenName = e.NewName) exboyfriends )
|> Event.listen (fun e ->
printfn "-- No %s's allowed!" e.NewName
e.Cancel <- true)