codefab.io.ElmSharp
0.0.1-beta
Prefix Reserved
See the version list below for details.
dotnet add package codefab.io.ElmSharp --version 0.0.1-beta
NuGet\Install-Package codefab.io.ElmSharp -Version 0.0.1-beta
<PackageReference Include="codefab.io.ElmSharp" Version="0.0.1-beta" />
paket add codefab.io.ElmSharp --version 0.0.1-beta
#r "nuget: codefab.io.ElmSharp, 0.0.1-beta"
// Install codefab.io.ElmSharp as a Cake Addin #addin nuget:?package=codefab.io.ElmSharp&version=0.0.1-beta&prerelease // Install codefab.io.ElmSharp as a Cake Tool #tool nuget:?package=codefab.io.ElmSharp&version=0.0.1-beta&prerelease
ElmSharp
π Welcome to ElmSharp.
I came across the Elm Language a few years ago and it has forever changed the way I approach software. Elm is multiple things: it is a language, it is a package ecosystem and more fundamentally it is an architecture.
I do find myself, however, sometimes finding it hard to describe to my fellow csharp colleagues how Elm works and what are the rules of the game. So I decided: what better way to show-and-tell than to "create Elm" in csharp.
Why am I so moved by the elm architecture? My experience is that it brings a set of healthy constraints that lead you towards objectively better software: many decisions that would be made later in a software project must be made earlier and more consciously. The practices this architecture enforces (immutability, pureness of functions, unidirectional data flow) align very well with the ultimate goal of having testable and reliable software.
The elm architecture is very well explained in the elm guide so I will use the bullet points I find more important:
- Your application has one single piece of state
- This state is immutable: the only way to "move state forward" is via the
Update
function - The
Update
function is always triggered by aMessage
- The only way to have a side-effect in the world is via a
Command
Message
s can come from theView
, fromSubscriptions
or from the result of aCommand
You can think of a Message
as a fact: "the user clicked on this button", "time has elapsed", "this HTTP request has failed", "the time is now 12:24:00".
You can think of a Command
as an intention to affect the world: "can you please run this HTTP request?", "can you please tell me the time?", "can you please give me a random number between 0 and 100?", "I would like a new Guid, please".
There are two worlds in an ElmSharp application: the runtime world and the user world. You are the user, the creator of awesome applications.
As a user, your job consists of:
Creating a
Model
. For instance if your application is a simple counter, yourModel
could bepublic record Model(int Count);
Declaring the list of
Message
s that your application understands. For the simple counter application, we can imagine three messages:IncrementClicked
,DecrementClicked
andResetClicked
Implementing an
Init
function which tells ElmSharp about the initial state when your application startsImplementing the
Subscriptions
function, which is the way that you let ElmSharp know "given my model is currently X, I want to subscribe to these interesting events about the world (or none)"Implementing the
View
function which is how you will represent yourModel
to the user. The whole view can only be dependent on data present in theModel
Implementing the
Update
function, which is how you make your model progress, in response to incomingMessage
s
βΉ More advanced use cases will require you to implement your specific
Command
andSubscription
but this is something we can cover in a later topic.
β How does it look like?
Assuming the following GlobalUsings.cs
in your project UserCode
:
// π GlobalUsings.cs
global using Cmd = ElmSharp.ElmSharp<UserCode.Model, UserCode.Message>.Command;
global using Sub = ElmSharp.ElmSharp<UserCode.Model, UserCode.Message>.Subscription;
These are the signatures of the important functions:
// MODEL (immutable; holds you application state)
public record Model(
(int Width, int Height)? ConsoleSize);
// MESSAGE (immutable; communicates facts that happened)
public abstract record Message
{
public sealed record OnKeyPressed(ConsoleKeyInfo KeyInfo) : Message { }
// ...;
}
// INIT (pure function; provides the initial state of the app and commands to execute)
public static (Model, Cmd) Init() => /*...*/;
// SUBSCRIPTIONS (pure function; allows you to obtain messages from non-user inputs)
public static ImmutableDictionary<string, Sub> Subscriptions(Model model) => /*...*/;
// VIEW (pure function; given a current model, return a visualization intention)
public static object View(Model model, Action<Message> dispatch) => /*...*/;
// UPDATE (pure function; the only way to move your state forward; gets triggered by incoming messages)
public static (Model, Cmd) Update(Message message, Model model) => message switch
{
Message.OnKeyPressed msg =>
model.OnKeyPressed(msg),
//...
}
// Using extension methods (pure functions) on Model, to allow for a cleaner looking Update function
internal static (Model, Cmd) OnKeyPressed(
this Model model,
Message.OnKeyPressed msg) => msg.KeyInfo.Key switch
{
ConsoleKey.UpArrow => /*...*/,
};
π Let's build: Guessing game
The premise of the game
In this application, we'll create a "game" where the computer "thinks" of a number between 0
and 9
and the player tries to guess the number. On each guess, the computer will tell the player if the number is greater or lower than the correct one, or if the player guessed the number. We'll use a console application for this example.
π’ Model
Our Model
will need to hold two values: the number to be guessed, and the player's current guess. We create a new Model.cs
file with the following:
// π Model.cs
namespace GuessingGame;
public record Model(
int NumberToBeGuessed,
int? CurrentPlayerGuess);
β Message
We now need our initial Message
declaration. It doesn't have to be complete, as our program will evolve over time. In fact, for now, we won't have any messages, and we'll add them as we go. So we create a Message.cs
file:
// π Message.cs
namespace GuessingGame;
public abstract record Message
{
}
π Global Usings
Now that we have both a Model
and a Message
we can create our GlobalUsings.cs
to make future code much easier:
// π GlobalUsings.cs
global using Cmd = ElmSharp.ElmSharp<GuessingGame.Model, GuessingGame.Message>.Command;
global using Sub = ElmSharp.ElmSharp<GuessingGame.Model, GuessingGame.Message>.Subscription;
β¨ Init
We can now create our Init
function. For the very first iteration of our code, we'll have the most boring game in the world, where the secret number is always 3
. (π€« it's our little secret, nobody will know).
Let's create an Init.cs
file, and due to the fact that csharp doesn't allow top-level functions, we cheat a little and create a static partial class ElmFuncs
. The partial
is due to the fact that we'll be using this ElmFuncs
(or whatever you decide to name it!) for other upcoming functions.
// π Init.cs
namespace GuessingGame;
public static partial class ElmFuncs
{
public static (Model, Cmd) Init() =>
(InitModel, InitCmd);
internal static Model InitModel { get; } =
new (NumberToBeGuessed: 3, CurrentPlayerGuess: null);
internal static Cmd InitCmd { get; } =
Cmd.None;
}
As a reminder, Init()
will be invoked by ElmSharp runtime once, when the application starts. This is where we let ElmSharp know what is the "zero" of our Model
, and what Command
s to run. Spoiler alert: for the next iteration of our game, we'll ask ElmSharp to give us a random number, instead of 3, but we'll get there.
β» Update
Given that we don't yet have any Message
defined in our game, our Update
function will pretty much be a no-op. Nevertheless, that is our starting point, so let's create it in Update.cs
. As before, we use the partial class ElmFuncs
trick:
// π Update.cs
namespace GuessingGame;
public static partial class ElmFuncs
{
public static (Model, Cmd) Update(Message message, Model model) =>
(model, Cmd.None);
}
In a normal (and upcoming) implementation of Update
we will be doing message switch { A => ..., B => ... };
but for now we don't have any messages declared yet, so the Update
is pretty much a constant no-op (meaning: keep the same model, and don't run any Command
).
β Subscriptions
Subscriptions are what allow us to hook up to "non-view events". Imagine that at some point we want to have a count-down where the player would only have a few seconds to choose a number, we could subscribe to Time and ElmSharp would send us a Message
whenever a certain amount of time elapses. For now we'll have an empty subscriptions, but we'll pretty soon have our first subscription to KeyPresses. As before, we leverage the partial class ElmFuncs
trick:
// π Subscriptions.cs
using System.Collections.Immutable;
namespace GuessingGame;
public static partial class ElmFuncs
{
public static ImmutableDictionary<string, Sub> Subscriptions(Model model) =>
Sub.None;
}
π View
The final piece of our game is our View
. In a normal elm application the View
function would return the desired Html to show in the browser but that is a luxury we don't have yet. So, the current version of ElmSharp expects the View
function to return a string
, otherwise...dragons happen π
ElmSharp will do a Console.Clear()
before rendering the result of the View
function, so that is something to keep in mind.
Let's create a View.cs
file and once again use the partial class ElmFuncs
trick:
// π View.cs
namespace GuessingGame;
public static partial class ElmFuncs
{
public static object View(Model model, Action<Message> dispatch) =>
$"\n" +
$" βββββββββββββββββ\n" +
$" β Guessing game β\n" +
$" βββββββββββββββββ\n" +
$"\n" +
$" Please choose a number between 0 and 9 or press [q] to Quit";
}
π£ Main
Okay, it is time to wire everything together and see if we get some output on our console. π€ fingers crossed!
Let's create a Program.cs
file and with the power of top-level statements it will look like this:
// π Program.cs
using ElmSharp;
using GuessingGame;
await ElmSharp<Model, Message>.Run(
init: ElmFuncs.Init,
update: ElmFuncs.Update,
view: ElmFuncs.View,
subscriptions: ElmFuncs.Subscriptions);
If we run the application we'll be able to see the initial screen of our game. But alas, it is a very boring game, since we can't guess a number, or even quit the application. Let's fix that πͺ
πͺ Add feature: quit the game
ElmSharp comes with some built-in commands, and the one we are interested in is StopApp
which can be obtained via Command.StopAppWithCode(int exitCode)
. This sounds like a perfect candidate for the "quit the game" feature.
However, we must keep two things in mind: Command
can only come from Init
or Update
. Given that we don't want our application to quit upon start, we must do it in the Update
function. The hurdle is that Update
can only be triggered by a Message
. So we need some kind of message that lets us know the player has pressed a key. For this we can use a Subscription
.
Our goal will be:
Create a new
Message
:PlayerPressedKey
Add a
Subscription
that will listed to keyboard key presses and send thisPlayerPressedKey
messageAdjust the
Update
function to react accordingly to our newPlayerPressedKey
message
Let's do this π€.
β Creating a new Message
// π Message.cs
namespace GuessingGame;
public abstract record Message
{
public sealed record PlayerPressedKey(ConsoleKeyInfo KeyInfo) : Message { }
}
Notice that the message can carry with it data. In this example the data that it carries is ConsoleKeyInfo KeyInfo
. The subscription will force, via the constructor that you specify a Message
which can carry this particular piece of data with it.
β Subscribing to ConsoleKeyPress
// π Subscriptions.cs
using System.Collections.Immutable;
namespace GuessingGame;
public static partial class ElmFuncs
{
public static ImmutableDictionary<string, Sub> Subscriptions(Model model) =>
ImmutableDictionary<string, Sub>
.Empty
.Add(nameof(Sub.ConsoleKeyPressSubscription),
new Sub.ConsoleKeyPressSubscription(
onKeyPress: keyInfo => new Message.PlayerPressedKey(keyInfo)));
}
Okay, wow that's a mouthful piece of code, let's break it down. The way subscriptions work is that ElmSharp will manage them for you, but by the key of the returned dictionary.
In this piece of code, we are simply returning a Dictionary
. On our code base, nothing happens in terms of subscriptions. It will be ElmSharp itself that keep track of "Hey, you didn't have a subscription named 'banana' before, so let me wire that up for you. Also, I notice that you no longer returned a subscription named 'cat-alert', so I'll tear it down for you.". Behind the scenes, ElmSharp uses Task
s and CancellationToken
s to clean everything up, but as an ElmSharp user this is not something you need to worry about. Just keep in mind that if you keep the same key
, ElmSharp won't make any subscription management for you.
π A potential cause for bugs: if you had a TimeSubscription (which takes a
TimeSpan
as the interval) which would depend on your model, you could fall victim of a bug. If you wish to change the TimeSpan, you should provide a new subscription name, so that ElmSharp can tear down the old subscription and wire up the new one with the new TimeSpan.
π§ Adjusting the Update
function
We now have a Message
to pattern match on, do let's add the code. We'll make it nice by leveraging an extension method on Model
so that we don't clutter the Update
function too much.
// π Update.cs
using static GuessingGame.Message;
namespace GuessingGame;
public static partial class ElmFuncs
{
public static (Model, Cmd) Update(Message message, Model model) => message switch
{
PlayerPressedKey msg =>
model.OnPlayerPressedKey(pressedKey: msg.KeyInfo.Key),
};
internal static (Model, Cmd) OnPlayerPressedKey(
this Model model,
ConsoleKey pressedKey) => pressedKey switch
{
ConsoleKey.Q =>
(model, Cmd.StopAppWithCode(exitCode: 0)),
// No-op for other key-presses
_ =>
(model, Cmd.None)
};
}
π Taking the app for a spin
Okay, so to recap we have setup a Subscription
to key presses, we have created a Message
to convey this key press, and we have adjusted our Update
function to return a StopAppCommand
when we see this Q
key being pressed. If all goes well, we should be able to start our application and press keys and "nothing should happen" (you might see the console flicker a little depending on your terminal), but once we press Q
on our keyboard, the app should exit.
At least that's how it works on my machine π
πΆββοΈ On to the next feature: guess a number
As always, before we implement a new feature we should think a little on how we are going to break it down and approach it. We already have the PlayerPressedKey
message, so that seems like a very good place to adjust our Model
with the player's guess. Also, we should adjust the View
function so that it shows to the player whether her guess is too high, too low of just right. For now we'll have unlimited guesses, and once the player guesses the number we should congratulate them and exit the application. Easy peasy, right?
π§ Adjusting the Update
function
We already have a match on Q
, let's add the matches on the numbers 0-9. C# has this nice syntax where we can pattern match on range, so we add >= ConsoleKey.D0 and <= ConsoleKey.D9
to our Update
/OnPlayerPressedKey
function. This is also the first time we are changing the model, so we get to leverage the with {}
syntax from records. Here is how Update.cs
looks:
// π Update.cs
using static GuessingGame.Message;
namespace GuessingGame;
public static partial class ElmFuncs
{
public static (Model, Cmd) Update(Message message, Model model) => message switch
{
PlayerPressedKey msg =>
model.OnPlayerPressedKey(pressedKey: msg.KeyInfo.Key),
};
internal static (Model, Cmd) OnPlayerPressedKey(
this Model model,
ConsoleKey pressedKey) => pressedKey switch
{
// Q is the key for quitting the app
ConsoleKey.Q =>
(model, Cmd.StopAppWithCode(exitCode: 0)),
// Numbers between 0 and 9
>= ConsoleKey.D0 and <= ConsoleKey.D9 =>
(model with { CurrentPlayerGuess = pressedKey - ConsoleKey.D0 }, Cmd.None),
// No-op for other key-presses
_ =>
(model, Cmd.None)
};
}
We should now modify our View
function, to show to the player what number she has guessed and if his guess is too high or too low.
π§ Adjusting the View
function
In our Model
we are currently using int? CurrentPlayerGuess
to hold either a null
if the player hasn't guessed a number yet, or an int
with the player's guess. Therefore we can have some branching logic on the View
to display accordingly. Like before, we can leverage extension methods on Model
to make the code a bit cleaner.
// π View.cs
namespace GuessingGame;
public static partial class ElmFuncs
{
public static object View(Model model, Action<Message> dispatch) =>
$"\n" +
$" βββββββββββββββββ\n" +
$" β Guessing game β\n" +
$" βββββββββββββββββ\n" +
$"\n" +
$" Please choose a number between 0 and 9 or press [q] to Quit\n" +
$"{model.PlayerGuessView()}";
internal static string PlayerGuessView(this Model model)
{
// The player hasn't made a guess yet
if (model.CurrentPlayerGuess is not int playerGuess)
return string.Empty;
var guessQuality =
(playerGuess < model.NumberToBeGuessed) ? "too low." :
(playerGuess > model.NumberToBeGuessed) ? "too high." :
"π Perfect! Congratulations!";
return $"\n You guessed [{playerGuess}]. Your guess is {guessQuality}\n";
}
}
Depending on the terminal that you are using, the
π
emoji might either be correctly rendered or displayed as??
. Emojis are hard, what can I tell you π€·ββοΈ.
π Taking the app for another spin
Okay, it is time to take this phenomenal game for a spin once again and see how it works. We can see that any input other than 0-9
or Q
has no effect, and if we press the numbers on our keyboard (bug spoiler alert, the ones above the major key section, not the ones on the numpad to the right) we can see the game telling us "too high", "too low" or congratulating us for finding the ""secret"" number (wink wink π). We can note two improvements that we can make on our app:
The numbers on the numpad should also work as well
The game should exit once the player has found the correct number
π§ Allowing the numpad numbers to be used
This one is quite easy, we just need to do a slight adjustment on our Update
function. It is a good opportunity to refactor some of the code, since we now have two vectors for guessing a number. After a slight refactor, this is how our Update.cs
looks like now:
// π Update.cs
using static GuessingGame.Message;
namespace GuessingGame;
public static partial class ElmFuncs
{
public static (Model, Cmd) Update(Message message, Model model) => message switch
{
PlayerPressedKey msg =>
model.OnPlayerPressedKey(pressedKey: msg.KeyInfo.Key),
};
internal static (Model, Cmd) OnPlayerPressedKey(
this Model model,
ConsoleKey pressedKey) => pressedKey switch
{
// Q is the key for quitting the app
ConsoleKey.Q =>
(model, Cmd.StopAppWithCode(exitCode: 0)),
// Numbers between 0 and 9
>= ConsoleKey.D0 and <= ConsoleKey.D9 =>
model.WithPlayerGuess(pressedKey - ConsoleKey.D0),
// NumPad numbers between 0 and 9
>= ConsoleKey.NumPad0 and <=ConsoleKey.NumPad9 =>
model.WithPlayerGuess(pressedKey - ConsoleKey.NumPad0),
// No-op for other key-presses
_ =>
(model, Cmd.None)
};
internal static (Model, Cmd) WithPlayerGuess(this Model model, int playerGuess) =>
(model with { CurrentPlayerGuess = playerGuess }, Cmd.None);
}
Exiting the game once the player found the correct number
This is an interesting addition for two reasons:
If we would just return the
Cmd.StopAppWithCode(exitCode: 0)
from theUpdate
function once the player guesses the correct number, then ElmSharp wouldn't render theView
congratulating the player. Therefore we might want to have a 1 second timeout between the player guessing the number and the application exiting, to have a better experience (a normal game would have some sound or animation to celebrate)If we add a timeout between the correct guessing and the game exiting, there is a chance the player presses another guess. That would be awkward, so we should adjust our
Update
function to no longer take any guesses after we have the correct one
Therefore, here we go again.
π« No longer taking guesses after the correct guess
For this one we simply need to adjust our Update
function, specifically the WithPlayerGuess
function, where we short-circuit early if the guess already matched before:
// π Update.cs
// ...
internal static (Model, Cmd) WithPlayerGuess(this Model model, int playerGuess) =>
model.NumberToBeGuessed == model.CurrentPlayerGuess
// The player had already guessed the number, we don't change the model
? (model, Cmd.None)
// Otherwise, adjust the model with the player's guess
: (model with { CurrentPlayerGuess = playerGuess }, Cmd.None);
// ...
In this change I am using the ternary conditional operator but this is a syntax preference of mine. You could use the more "normal" if ... return
syntax if you prefer π.
π Exiting the game a few seconds after the correct guess
For exiting the game a few seconds after the correct guess, these are the high-level steps that we need to take:
Create a new
Message
(example:TimeoutElapsed
) to be triggered after a certain timeout (remember: exiting the application is aCommand
and we can only issue commands as a return ofInit
orUpdate
. We need aMessage
to trigger theUpdate
hence we need to create a new oneWe need a new handler on the
Update
function to handle this newTimeoutElapsed
message. This is the handler that will return the stop application commandFinally, on the
Update
function, when we see the user has correctly guessed the number, we return aCommand
that asks ElmSharp "Please send me the messageTimeoutElapsed
after n seconds have elapsed"
Let's do this πͺ
β Creating a new TimeoutElapsed
message
On our Message.cs
we add this new TimeoutElapsed
record:
// π Message.cs
namespace GuessingGame;
public abstract record Message
{
public sealed record PlayerPressedKey(ConsoleKeyInfo KeyInfo) : Message { }
public sealed record TimeoutElapsed : Message { }
}
π§ Adjusting the Update
function to handle TimeoutElapsed
This should be getting a bit easier and more mechanical by now. Here is our updated Update.cs
file, where we now handle the TimeoutElapsed
message:
// π Update.cs
// ...
public static (Model, Cmd) Update(Message message, Model model) => message switch
{
//...
TimeoutElapsed =>
model.OnTimeoutElapsed(),
//...
};
//...
internal static (Model, Cmd) OnTimeoutElapsed(
this Model model) =>
(model, Cmd.StopAppWithCode(exitCode: 0));
// ...
π§ Adjusting the Update
function to return SetTimeoutCommand
when the player guesses the number
ElmSharp has a few built-in commands, and we are now interested on the SetTimeoutCommand
. Let's return this command from our Update
function when we detect that the player has entered the correct guess:
// π Update.cs
// ...
internal static (Model, Cmd) WithPlayerGuess(this Model model, int playerGuess)
{
// The player had already guessed the number, we don't change the model (ignore the new guess)
if (model.NumberToBeGuessed == model.CurrentPlayerGuess)
return (model, Cmd.None);
// We check if the player has guessed the number. If so, we set a timeout to be notified later
var command = model.NumberToBeGuessed != playerGuess
? Cmd.None
: new Cmd.SetTimeoutCommand(
timeoutDuration: TimeSpan.FromSeconds(2),
onTimeoutElapsed: () => new TimeoutElapsed());
return (model with { CurrentPlayerGuess = playerGuess }, command);
}
// ...
π Taking the app for yet another spin
By now our game has a few features (but it's not complete yet!). The player can exit the game. The player can guess a number and the game will let the player know if their guess is too high or too low. And finally, the game congratulates the player for finding the correct number and gracefully exits after a period of time.
At least that's how it works on my machine π
There is only one final thing to do for now: we need a way for the secret number to not always be 3
. Where could we implement such a feature?
Yes, in the Init
function. But remember, the Init
function, just like all the others, needs to be pure.
Did you notice that all the functions we wrote so far are pure? A pure function is a function that given the same inputs will always return the same outputs. Which is a fancy way of saying it has no "tentacles" or dependencies to the external world/state. We accomplish this by not using impure methods, such as
DateTime.Now
orRandom
orGuid.NewGuid()
etc. Every time we need to do such impure business we use aCommand
to do it, and the command generates a pureMessage
so that we can get back to a pure implementation ofUpdate
. This applies to everything, HTTP requests, randomness, datetime, etc. If you think about it, and you have some TDD experience, you will notice that TDD compels you to remove all impurity from your methods, so they can be instrumented and tested. What we have accomplished with ElmSharp is an architecture (the elm architecture) that forces us to stay pure. I guess you can see how testing these pure functions then becomes a trivial matter: no dependency injection, no fancy business: you construct aModel
, you construct aMessage
, invoke theUpdate
function and assert against the output. Same thing goes for theView
function. Notice howUpdate
isn't evenasync/await
because it really doesn't have the capability of going out into the world and do ..who knows what... Fun stuff, no? π
Ah, back to the problem at hand, generating a random number between 0 and 9. Let's use another built-in command: Cmd.GetRandomNumberCommand
which leverages the System.Security.Cryptography.RandomNumberGenerator
to do its thing.
Of course this also means we need a new Message
to get the new secret number into our Model
(via the Update
function).
β Adding a SecretNumberPicked
message
You know the drill by now π
// π Message.cs
namespace GuessingGame;
public abstract record Message
{
public sealed record SecretNumberPicked(int SecretNumber) : Message { }
// ...
}
π§ Adjust the Init
function to request a random number
// π Init.cs
namespace GuessingGame;
public static partial class ElmFuncs
{
public static (Model, Cmd) Init() =>
(InitModel, InitCmd);
internal static Model InitModel { get; } =
// We'll use -1 as init. This will be updated by the result of GetRandomNumberCommand
new(NumberToBeGuessed: -1,
CurrentPlayerGuess: null);
internal static Cmd InitCmd { get; } =
new Cmd.GetRandomNumberCommand(
fromInclusive: 0,
toExclusive: 10,
onRandomNumberGenerated: number => new Message.SecretNumberPicked(number));
}
π§ Handling the SecretNumberPicked
in the Update
function
Hopefully this is getting easier by the minute:
// π Update.cs
// ...
public static (Model, Cmd) Update(Message message, Model model) => message switch
{
SecretNumberPicked msg =>
model.OnSecretNumberPicked(msg.SecretNumber),
// ...
};
internal static (Model, Cmd) OnSecretNumberPicked(this Model model, int secretNumber) =>
(model with { NumberToBeGuessed = secretNumber }, Cmd.None);
// ...
π Taking the app for the final spin
If you take the app for a spin now, you should see that, unless you are very unlucky, 3
is no longer the secret number. You can now play the game as expected and see how long it takes you to find a random number between 0 and 9.
In the end, the game wasn't the goal: the goal was to get a clearer understanding of the elm architecture and how you can leverage it for the challenges that lie ahead. As said many times throughout the industry, there are no silver bullets. This isn't an answer to all problems, but I do find the constraints quite liberating. I can understand if you have feelings of "boilerplate overkill" due to having to create a Message
and then modify the Update
and then the View
etc, but if you have a honest look, this is also what must be done in any decent size software project.
I hope you enjoy using ElmSharp, as much as I enjoyed creating it.
Product | Versions Compatible and additional computed target framework versions. |
---|---|
.NET | net6.0 is compatible. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. |
-
net6.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
Version | Downloads | Last updated |
---|---|---|
0.0.2-beta | 102 | 8/25/2023 |
0.0.1-beta | 83 | 8/25/2023 |
(BETA software)
An initial release to gather feedback from members of the community