codefab.io.ElmSharp 0.0.1-beta

The ID prefix of this package has been reserved for one of the owners of this package by NuGet.org. Prefix Reserved
This is a prerelease version of codefab.io.ElmSharp.
There is a newer prerelease version of this package available.
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
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="codefab.io.ElmSharp" Version="0.0.1-beta" />
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add codefab.io.ElmSharp --version 0.0.1-beta
#r "nuget: codefab.io.ElmSharp, 0.0.1-beta"
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
// 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 a Message
  • The only way to have a side-effect in the world is via a Command
  • Messages can come from the View, from Subscriptions or from the result of a Command

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, your Model could be public record Model(int Count);

  • Declaring the list of Messages that your application understands. For the simple counter application, we can imagine three messages: IncrementClicked, DecrementClicked and ResetClicked

  • Implementing an Init function which tells ElmSharp about the initial state when your application starts

  • Implementing 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 your Model to the user. The whole view can only be dependent on data present in the Model

  • Implementing the Update function, which is how you make your model progress, in response to incoming Messages

β„Ή More advanced use cases will require you to implement your specific Command and Subscription 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 Commands 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 this PlayerPressedKey message

  • Adjust the Update function to react accordingly to our new PlayerPressedKey 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 Tasks and CancellationTokens 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 the Update function once the player guesses the correct number, then ElmSharp wouldn't render the View 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 a Command and we can only issue commands as a return of Init or Update. We need a Message to trigger the Update hence we need to create a new one

  • We need a new handler on the Update function to handle this new TimeoutElapsed message. This is the handler that will return the stop application command

  • Finally, on the Update function, when we see the user has correctly guessed the number, we return a Command that asks ElmSharp "Please send me the message TimeoutElapsed 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 or Random or Guid.NewGuid() etc. Every time we need to do such impure business we use a Command to do it, and the command generates a pure Message so that we can get back to a pure implementation of Update. 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 a Model, you construct a Message, invoke the Update function and assert against the output. Same thing goes for the View function. Notice how Update isn't even async/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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • 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 87 8/25/2023
0.0.1-beta 71 8/25/2023

(BETA software)
An initial release to gather feedback from members of the community