W poprzedniej części wątku WPFowego projektu MemoryVisualizer skupiłem się na przypomnieniu, czym jest i jak implementować komendę (interfejs ICommand) w C#. Dla przypomnienia, napisałem ogólne rozwiązanie, któremu podaje się odpowiednie metody, dzięki czemu jest całkowicie reużywalne:
public class CommandHandler : ICommand
{
private Action action;
private Func<bool> canExecute;
public CommandHandler(Action action, Func<bool> canExecute)
{
this.action = action;
this.canExecute = canExecute;
}
public event EventHandler CanExecuteChanged;
public bool CanExecute(object parameter)
{
return canExecute();
}
public void Execute(object parameter)
{
action();
}
}
Muszę to przełożyć na F#, żeby używać komend w MemoryVisualizerze. Tutaj zaś pojawiają się pewne pytania, nad którymi muszę się trochę pochylić. Co prawda odpowiedzi mogłyby paść od razu, ale warto przy okazji temat zrozumieć trochę głębiej. Otóż chodzi o eventy i delegaty w C#, które to widzimy w powyższym kodzie.
Nie wiem czy wiecie, ale czegoś takiego jak event w CLR nie ma - nie znajdziemy żadnej instrukcji IL odpowiedzialnej za ich obsługę. Jest to tylko i wyłącznie tzw. cukier syntaktyczny (ang. syntactic sugar) języka C# i VB.NET. Są natomiast o eventach dostępne metadane, ale to w tym momencie nie jest istotne.
Co się zatem dzieje, gdy w C# zdefiniujemy taki event?
public event EventHandler CanExecuteChanged;
Jest on tłumaczony, w przybliżeniu (w rzeczywistości jest to bardziej skomplikowane ze względu na wielowątkowość), na coś takiego:
private EventHandler CanExecuteChanged;
public add_CanExecuteChanged(EventHandler @value)
{
this.CanExecuteChanged = (EventHandler)Delegate.Combine(this.CanExecuteChanged, value);
}
public remove_CanExecuteChanged(EventHandler @value)
{
this.CanExecuteChanged = (EventHandler)Delegate.Remove(this.CanExecuteChanged, value);
}
Czyli jest to nic innego jak operowanie na polu
CanExecuteChanged, który jest typu
EventHandler, dziedziczącego zresztą po
MulticastDelegate.
EventHandler jest delegatem:
public delegate void EventHandler(object sender, EventArgs e);
Oki. Czyli podsumowując, event jest nakładką na MulticastDelegate wraz z dwoma pomocnicznymi metodami.
Jak to się zatem wszystko ma do F#? Zacznijmy od tego co się stanie, gdy stworzymy typ implementujący ICommand, ale bez implementacji eventa CanExecuteChanged:
type CommandHandler (action:(obj -> unit), canExecute:(obj -> bool)) =
interface ICommand with
member x.CanExecute arg = canExecute(arg)
member x.Execute arg = action(arg)
Po próbie kompilacji dostaniemy błąd:
Error: No implementation was given for 'ICommand.add_CanExecuteChanged(value: EventHandler) : unit'.
Error: No implementation was given for 'ICommand.remove_CanExecuteChanged(value: EventHandler) : unit'.
Brzmi znajomo! Zatem potrzebujemy jakiegoś odpowiednika eventu w F#, bo trudno uwierzyć, byśmy te metody musieli implementować ręcznie. Intellisense odnośnie interfejsu ICommand podpowiada, że x.CanExecuteChanged jest to event CanExecuteChanged : EventHandler, zatem wpisujemy:
type CommandHandler (action:(obj -> unit), canExecute:(obj -> bool)) =
interface ICommand with
event x.CanExecuteChanged : EventHandler
member x.CanExecute arg = canExecute(arg)
member x.Execute arg = action(arg)
I wszystko się wywala - F# nie zna słówka event. Więc po co o nim mówi Intellisense?! Dobrze, RTFM. MSDN ładnie mówi o evantach, że:
F# events are represented by the F# Event class, which implements the IEvent interface. IEvent is itself an interface that combines the functionality of two other interfaces, IObservable<T> and IDelegateEvent. Therefore, Events have the equivalent functionality of delegates in other languages, plus the additional functionality from IObservable, which means that F# events support event filtering and using F# first-class functions and lambda expressions as event handlers. This functionality is provided in the Event module.
Zatem eventy w F# są czymś znacznie więcej, niż prostymi opakowaniami delegatów. Spójrzmy na definicję oraz dokumentację wspomnianego IDelegateEvent:
type IDelegateEvent<'Delegate> =
interface
abstract this.AddHandler : 'Delegate -> unit
abstract this.RemoveHandler : 'Delegate -> unit
end
F# gives special status to member properties compatible with type IDelegateEvent and tagged with the CLIEventAttribute. In this case the F# compiler generates approriate CLI metadata to make the member appear to other CLI languages as a CLI event.
Wygląda obiecująco! Ponieważ pojęcie delagatu również w F# występuje i dla naszego przykładu ma definicję:
type EventHandler =
delegate of obj * EventArgs -> unit
To wygląda, jakby Event<EventHandler> było tym, czego szukamy! Radośnie piszemy więc coś takiego:
type CommandHandler (action:(obj -> unit), canExecute:(obj -> bool)) =
interface ICommand with
[<CLIEvent>]
member x.CanExecuteChanged = new Event<EventHandler>()
member x.CanExecute arg = canExecute(arg)
member x.Execute arg = action(arg)
I... dostajemy błędy kompilacji (odnośnie
new Event<EventHandler>()):
Error: The field, constructor or member 'AddHandler' is not defined
Error: The field, constructor or member 'RemoveHandler' is not defined
Zatem użycie atrybutu CLIEvent na property sprawia, że kompilator oczekuje implementacji wspomnianych metod (a prawdopodobnie implementacji IDelegateEvent). Popatrzmy zatem czym właściwie jest ten cały Event<>:
type Event<'T> =
/// Creates an observable object.
new : unit -> Event<'T>
/// Publishes an observation as a first class value.
member Publish : IEvent<'T>
/// Triggers an observation using the given parameters.
/// arg: The event parameters.
member Trigger : arg:'T -> unit
Ciekawe! Czyli tak naprawdę
Event nie dostarcza potrzebnej nam implementacji
IEvent, tylko posiada property (
Publish), który to robi! Spróbujmy zatem:
type CommandHandler (action:(obj -> unit), canExecute:(obj -> bool)) =
member x.event = new Event<EventHandler>()
interface ICommand with
[<CLIEvent>]
member x.CanExecuteChanged = x.event.Publish
member x.CanExecute arg = canExecute(arg)
member x.Execute arg = action(arg)
Kompilujemy i... błąd, dwukrotny, wskazujący na kod x.event.Publish:
Error: This expression was expected to have type Handler<EventHandler> but here has type EventHandler.
Ręce opadają. Pojawia się nowy element tej układanki, Handler<>, opisany jako:
A delegate type associated with the F# event type IEvent.
type Handler<'T> =
delegate of sender:obj * args:'T -> unit
Zatem można sobie w głowie poukładać, że
EventHandler to jest
Handler<EventArgs>? Do niczego to nie prowadzi, zwłaszcza że automatyczne rozpoznawanie typów w F# miesza tu w głowach. Wędrując zrezygnowany po MSDN natknąłem się na
DelagateEvent:type DelegateEvent<'Delegate> =
class
new DelegateEvent : unit -> DelegateEvent<'Delegate>
member this.Trigger : obj [] -> unit
member this.Publish : IDelegateEvent<'Delegate>
end
Zatem to może to? Nie zwykły Event, który teoretycznie powinien implementować co trzeba, ale właśnie ten typ? Trochę zrezygnowany, spróbowałem:
type CommandHandler (action:(obj -> unit), canExecute:(obj -> bool)) =
member x.event = new DelegateEvent<EventHandler>()
interface ICommand with
[<CLIEvent>]
member x.CanExecuteChanged = x.event.Publish
member x.CanExecute arg = canExecute(arg)
member x.Execute arg = action(arg)
Alleluja! Skompilowało się i działa! To była bardzo długa podróż…
Na chwilę obecną jestem zawiedziony. Dojście do tego w sposób intuicyjny jest praktycznie niemożliwe. Kontrola typu, mimo statycznie typowanego języka, niewiele pomogła. Tak wiem, że to przez wpychanie w F# zaszłości z .NETa typu interfejsowe eventy. Ale jednak trochę tu biegania wokół własnego ogona. Dokumentacja MSDN też praktycznie nie pomaga. Ćwiczenie to miało mi głównie pokazać jak jest z jakością komunikatów o błędach i dokumentacją. Jak na razie 3 “z dwoma”.
PS. Oczywiście, znalazłem potem w książkach jak to zaimplementować, ale chciałem dojść do tego sam, by zrozumieć.