Skip to content

Instantly share code, notes, and snippets.

@programmation
Created August 7, 2020 02:11
Show Gist options
  • Save programmation/7686241052b38de37222a6d8a7a7d476 to your computer and use it in GitHub Desktop.
Save programmation/7686241052b38de37222a6d8a7a7d476 to your computer and use it in GitHub Desktop.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Threading.Tasks.Dataflow;
using ICountingTransform = Messaging.ITransform<Messaging.CountingMessage, Messaging.CountingState>;
using ICountingSideEffect = Messaging.ISideEffect<Messaging.CountingView, Messaging.CountingMessage, Messaging.CountingState>;
namespace Messaging
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
}
}
public class StateHistory<TMessage, TState>
where TMessage : Message
where TState : State
{
public DateTime When { get; }
public TState State { get; }
public TMessage? Message { get; }
public StateHistory(TState state, TMessage? message = null)
{
State = state;
Message = message;
When = DateTime.Now;
}
}
// Abstract stateful view types
public abstract record State { }
public abstract record Message { }
public abstract class View { }
public interface ITransform<TMessage, TState>
where TMessage : Message
where TState : State
{
IList<StateHistory<TMessage, TState>> StateHistory { get; }
void Initialize(TState initialState);
TState Transform(TMessage message, TState state);
}
public interface ISideEffect<TStatefulView, TMessage, TState>
where TStatefulView : StatefulView<TStatefulView, TMessage, TState>
where TMessage : Message
where TState : State
{
void AddSideEffect<T>(Action<TStatefulView, TMessage, TState, TState> action) where T : TMessage;
void RemoveSideEffect<T>() where T : TMessage;
void Run(TStatefulView statefulView, TMessage message, TState oldState, TState newState);
}
public abstract class StatefulView<TStatefulView, TMessage, TState>
where TStatefulView : StatefulView<TStatefulView, TMessage, TState>
where TMessage : Message
where TState : State
{
public ActionBlock<TMessage> Queue { get; private set; }
public TState State { get; private set; } = null!;
public ITransform<TMessage, TState> Transform { get; private set; } = null!;
public ISideEffect<TStatefulView, TMessage, TState>? SideEffect { get; private set; } = null!;
private StatefulView()
{
Queue = new ActionBlock<TMessage>(ProcessMessage);
State = InitialState;
}
public StatefulView(ITransform<TMessage, TState> transform)
: this()
{
Transform = transform;
Transform.Initialize(State);
}
public StatefulView(ITransform<TMessage, TState> transform, ISideEffect<TStatefulView, TMessage, TState> sideEffect)
: this(transform)
{
SideEffect = sideEffect;
}
public void Dispatch(TMessage message)
{
Queue.Post(message);
}
public void ProcessMessage(TMessage message)
{
var oldState = State;
var newState = Transform.Transform(message, oldState);
SideEffect?.Run((TStatefulView)this, message, oldState, newState);
State = newState;
}
public abstract TState InitialState { get; }
public abstract View View(TState state);
}
public abstract class StateTransform<TMessage, TState> : ITransform<TMessage, TState>
where TMessage : Message
where TState : State
{
public IList<StateHistory<TMessage, TState>> StateHistory { get; private set; }
= new List<StateHistory<TMessage, TState>>();
public void Initialize(TState initialState)
{
StateHistory.Add(new StateHistory<TMessage, TState>(initialState));
}
public virtual TState Transform(TMessage message, TState state)
{
return Remember(message, state);
}
protected virtual TState Remember(TMessage message, TState state)
{
StateHistory.Add(new StateHistory<TMessage, TState>(state, message));
return state;
}
}
public abstract class SideEffect<TStatefulView, TMessage, TState>
: ISideEffect<TStatefulView, TMessage, TState>
where TStatefulView : StatefulView<TStatefulView, TMessage, TState>
where TMessage : Message
where TState : State
{
private readonly IDictionary<Type, Action<TStatefulView, TMessage, TState, TState>> Actions
= new Dictionary<Type, Action<TStatefulView, TMessage, TState, TState>>();
public void AddSideEffect<T>(Action<TStatefulView, TMessage, TState, TState> action)
where T : TMessage
{
Actions[typeof(T)] = action;
}
public void RemoveSideEffect<T>()
where T : TMessage
{
Actions.Remove(typeof(T));
}
public void Run(TStatefulView statefulView, TMessage message, TState oldState, TState newState)
{
var type = message.GetType();
if (!Actions.ContainsKey(type))
return;
var action = Actions[type];
action.Invoke(statefulView, message, oldState, newState);
}
}
// Application state
public record CountingState : State
{
public static CountingState Initial()
=> new CountingState(1);
public CountingState(int step)
{
Step = step;
}
public int Count { get; set; }
public int Step { get; set; }
public bool IsTimerEnabled { get; set; }
}
// Application messages
public abstract record CountingMessage : Message { }
public record IncrementMessage : CountingMessage { }
public record DecrementMessage : CountingMessage { }
public record ResetMessage : CountingMessage { }
public record SetStepMessage : CountingMessage
{
public int Step { get; }
public SetStepMessage(int step)
{
Step = step;
}
}
public record TimerOnMessage : CountingMessage
{
public int Ticks { get; }
public TimerOnMessage(int ticks)
{
Ticks = ticks;
}
}
public record TimerOffMessage : CountingMessage { }
public record TimerTickMessage : CountingMessage { }
// Convenient properties and constructors
// -- Messages with no parameters can be static properties
// -- Messages with parameters need to be a new object for each invocation
public static class Messages
{
public static IncrementMessage IncrementMessage { get; } = new IncrementMessage();
public static DecrementMessage DecrementMessage { get; } = new DecrementMessage();
public static SetStepMessage SetStepMessage(int step) => new SetStepMessage(step);
public static TimerOnMessage TimerOnMessage(int ticks) => new TimerOnMessage(ticks);
public static TimerOffMessage TimerOffMessage { get; } = new TimerOffMessage();
public static TimerTickMessage TimerTickMessage { get; } = new TimerTickMessage();
public static ResetMessage ResetMessage { get; } = new ResetMessage();
}
// Message transformer
public class CountingTransform : StateTransform<CountingMessage, CountingState>
{
public override CountingState Transform(CountingMessage message, CountingState state)
{
var newState = message switch
{
IncrementMessage => state with { Count = state.Count + state.Step },
DecrementMessage => state with { Count = state.Count - state.Step },
SetStepMessage m => state with { Step = m.Step },
TimerOnMessage => state with { IsTimerEnabled = true },
TimerOffMessage => state with { IsTimerEnabled = false },
TimerTickMessage => state.IsTimerEnabled
? Transform(Messages.IncrementMessage, state)
: state,
ResetMessage => StateHistory[0].State,
_ => state
};
return base.Transform(message, newState);
}
}
// Stateful view implementation
public class CountingView :
StatefulView<CountingView, CountingMessage, CountingState>
{
public override CountingState InitialState => CountingState.Initial();
public CountingView(ICountingTransform transform)
: base(transform)
{
}
public CountingView(ICountingTransform transform, ICountingSideEffect sideEffect)
: base(transform, sideEffect)
{
}
// View and active objects
public override View View(CountingState state)
=> new VStack
{
new Text($"{state.Count}"),
new Button("Increment", () => Dispatch(Messages.IncrementMessage)),
new Button("Decrement", () => Dispatch(Messages.DecrementMessage)),
new HStack
{
new Text("Timer"),
new Toggle(state.IsTimerEnabled,
(bool isEnabled)
=> Dispatch(isEnabled
? (CountingMessage)Messages.TimerOnMessage(100)
: Messages.TimerOffMessage))
},
new Slider(state.Step, 10, (int step) => Dispatch(Messages.SetStepMessage(step))),
new Text($"Step Size: {state.Step}"),
new Button("Reset", () => Dispatch(Messages.ResetMessage))
};
}
public class CountingSideEffect : SideEffect<CountingView, CountingMessage, CountingState>
{
private CountingTimer _timer = null!;
public CountingSideEffect()
{
AddSideEffect<TimerOnMessage>(TimerOnSideEffect);
AddSideEffect<TimerOffMessage>(TimerOffSideEffect);
AddSideEffect<ResetMessage>(HandleReset);
}
private void TimerOnSideEffect(CountingView view, CountingMessage message, CountingState oldState, CountingState newState)
{
if (!(message is TimerOnMessage timerOnMessage))
return;
if (oldState.IsTimerEnabled)
return;
if (newState.IsTimerEnabled)
{
_timer = new CountingTimer(timerOnMessage.Ticks, () => view.Dispatch(Messages.TimerTickMessage));
_timer.Start();
}
}
private void TimerOffSideEffect(CountingView view, CountingMessage message, CountingState oldState, CountingState newState)
{
if (!(message is TimerOffMessage))
return;
if (!oldState.IsTimerEnabled)
return;
if (!newState.IsTimerEnabled)
{
_timer.Stop();
_timer = null!;
}
}
private void HandleReset(CountingView view, CountingMessage message, CountingState oldState, CountingState newState)
{
if (oldState.IsTimerEnabled)
{
_timer.Stop();
_timer = null!;
}
}
}
public class CountingTimer
{
private ActionBlock<int> _queue;
private bool _isRunning;
public int Delay { get; }
public Action Callback { get; }
public CountingTimer(int delay, Action callback)
{
Delay = delay;
Callback = callback;
_queue = new ActionBlock<int>(HandleAction);
}
public void Start()
{
if (_isRunning)
return;
_isRunning = true;
_queue.Post(Delay);
}
private void HandleAction(int delay)
{
if (!_isRunning)
return;
Task.Delay(delay)
.ContinueWith(t =>
{
if (!_isRunning)
return;
Callback?.Invoke();
_queue.Post(delay);
});
}
public void Stop()
{
if (!_isRunning)
return;
_isRunning = false;
}
}
// Demo View classes
public class HStack : View, IEnumerable<View>
{
public IEnumerator<View> GetEnumerator()
{
throw new NotImplementedException();
}
IEnumerator IEnumerable.GetEnumerator()
{
throw new NotImplementedException();
}
public void Add(View view) { }
}
public class VStack : View, IEnumerable<View>
{
public IEnumerator<View> GetEnumerator()
{
throw new NotImplementedException();
}
IEnumerator IEnumerable.GetEnumerator()
{
throw new NotImplementedException();
}
public void Add(View view) { }
}
public class Text : View
{
public Text(string text) { }
}
public class Toggle : View
{
public Toggle(bool isEnabled, Action<bool> trigger) { }
}
public class Button : View
{
public Button(string text, Action trigger) { }
}
public class Slider : View
{
public Slider(int step, int through, Action<int> onEditingChanged) { }
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment