Permalink
Switch branches/tags
Nothing to show
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
279 lines (216 sloc) 9.05 KB
packages uti id title platforms
id version
MathNet.Numerics.Core
3.17.0
id version
Neteril.ComputationExpression
0.2.1
id version
OxyPlot.Core
2.0.0-unstable1013
com.xamarin.workbook
744cd41b-7a5b-44a4-b17d-80a4297f0fc3
Computation Expression examples
DotNetCore
#r "Neteril.ComputationExpression"

using System;
using System.Runtime.CompilerServices;
using Neteril.ComputationExpression;
// Import shorter "operator" names, currently those are
//  - CxRun as a shortand for ComputationExpression.Run
//  - CxYield as a shortand for ComputationExpression.Yield
using static Neteril.ComputationExpression.Operators;

The Maybe workflow with Option<T>

Option<T> can have two outcomes None or Some<T>, only the Some case contains an actual value where None can be used to represent a potential error state.

Working with Option<T> would normally be a bit of pain because you would have to type check it manually to extra a potential value and do multiple if statements if doing successive statements using Option<T>.

Using the associated computation expression makes it much easier by automatically abstracting away both the value unwrapping and the if serie in case a None appears in the computation stream.

First here are some extra helpers to work with Option<T> that we will use in the example:

// The Option monad is given in the library
using Neteril.ComputationExpression.Instances;

static Option<int> TryDivide (int up, int down)
{
	Console.WriteLine ($"Trying to execute division {up}/{down}");
	if (down == 0)
		return None<int>.Value;
	return Some.Of (up / down);
}

static void PrintResult<T> (Option<T> maybe)
{
	switch (maybe)
	{
		case None<T> n:
			Console.WriteLine ("None");
			break;
		case Some<T> s:
			Console.WriteLine ($"Some {(T)s}");
			break;
	}
}

In this example we are executing a succession of division, modeling the error case of dividing by zero (usually an exception in C#) by instead returning None. When that happens, the computation expression will short-circuit the rest of the statements and simply return None directly.

Console.WriteLine ("Good example");
var good = CxRun<int, Option<int>> (new OptionExpressionBuilder (), async () => {
	var val1 = await TryDivide (120, 2);
	var val2 = await TryDivide (val1, 2);
	var val3 = await TryDivide (val2, 2);

	return val3;
});
PrintResult (good);

Console.WriteLine ();
Console.WriteLine ("Bad example");
var bad = CxRun<int, Option<int>> (new OptionExpressionBuilder (), async () => {
	var val1 = await TryDivide (120, 2);
	var val2 = await TryDivide (val1, 0);
	var val3 = await TryDivide (val2, 2);

	return val3;
});
PrintResult (bad);

If you run that final block of code your output should like this:

Good example

Trying to execute division 120/2
Trying to execute division 60/2
Trying to execute division 30/2
Some 15

Bad example

Trying to execute division 120/2
Trying to execute division 60/0
None

Re-creating yield state machine

We can also end up re-creating our good old yield return but with async/await and some help from the extra Combine operation of our computation expression builder. The result is somewhat more verbose but it’s doable. The code is using the built-in EnumerableMonad type.

#pragma warning disable 4014

// The Enumerable monad is given in the library
using Neteril.ComputationExpression.Instances;

var result = CxRun<int, EnumerableMonad<int>> (new EnumerableExpressionBuilder (), async () => {
	var item = await (EnumerableMonad<int>)new [] { 1, 2, 3 };
	var item2 = await (EnumerableMonad<int>)new [] { 100, 200 };
	// We want back a enumeration containing the concatenation of (item, item2, item1 * item2)
	// for all successive values of item1 and item2
	await CxYield (item);
	await CxYield (item2);
	return item * item2;
});
string.Join (", ", result.Select (i => i.ToString ()));

The above output should be [ 1, 100, 100, 1, 200, 200, 2, 100, 200, 2, 200, 400, 3, 100, 300, 3, 200, 600 ]

Haskell State monad

In Haskell pure world, state is not allowed to be mutated. Instead the intention is reproduced via the State<TState, TValue> monad that allows a piece of state to be propagated at the same time as intermediary results. This also shipped in the library.

The below sample borrows from the Haskell tutorial at https://en.wikibooks.org/wiki/Haskell/Understanding_monads/State

We are somewhat cheating in our case because where in Haskell it makes sense to pass the random value as state to be used as the next random seed, in C# it’s not really necessary since the before state is already encapsulated in the Random class.

// For Get and Put
using static Neteril.ComputationExpression.Instances.State;

static (int random, Random generator) RandomR ((int low, int high) interval, Random initialGenerator)
	=> (initialGenerator.Next (interval.low, interval.high), new Random ());

var rollDie = CxRun<int, State<Random, int>> (new StateExpressionBuilder<Random> (), async () => {
	var generator = await Get<Random> ();
	var (value, newGenerator) = RandomR ((1, 6), generator);
	await Put<Random, int> (newGenerator);
	return value;
});

EvalState<Random, int> (rollDie, new Random ());

The output of this code block will give you a random integer at the end.

Probabilities as monads

Probability distribution can be represented as monads and thus chained together. The example used here comes courtesy of https://www.chrisstucchio.com/blog/2016/probability_the_monad.html

Note that for execution time constraints, sampling count has been drastically reduced so that the histogram at the end can be generated in a reasonable amount of time (still can take a minute or two). Ultimately that means actual results are probably not that correct.

#r "MathNet.Numerics.Core"

using MathNet.Numerics.Distributions;
using MathNet.Numerics.Random;

public abstract class Probability<T> : IMonad<T>
{
	public abstract double Prob (T t);
}

public abstract class RandomSamplingProbablity : Probability<double>
{
	public abstract double Draw ();

	public override double Prob (double t)
	{ 
		const int NumSamples = 5000;
		var found = Enumerable
			.Range (0, NumSamples)
			.Where (i => Math.Abs (Draw () - t) < 0.001) // Shallow equality
			.Count ();
		var prob = ((double)found) / NumSamples;
		return prob;
	}
}

public class DiscreteDistributionProbability : Probability<int>
{
	IDiscreteDistribution distribution;

	public DiscreteDistributionProbability (IDiscreteDistribution d) => this.distribution = d;

	public override double Prob (int t) => distribution.Probability (t);
}

public class ContinuousDistributionProbability : RandomSamplingProbablity
{
	IContinuousDistribution distribution;

	public ContinuousDistributionProbability (IContinuousDistribution d) => this.distribution = d;

	public override double Draw () => distribution.Sample ();
}

public class ComposedProbability<T> : Probability<T>
{
	Func<T, double> prob;
	public ComposedProbability (Func<T, double> prob) => this.prob = prob;

	public override double Prob (T t) => prob (t);
}

// Returns "all" possible values of a given type
static IEnumerable<T> SpaceOf<T> ()
{
	if (typeof (T) == typeof (int))
		// The universal cast "operator"
		return (IEnumerable<T>)(object)Enumerable.Range (0, 100);
	if (typeof (T) == typeof (double))
		return (IEnumerable<T>)(object)DoubleRange (0, 1, 0.05);
	throw new NotSupportedException ();
}

static IEnumerable<double> DoubleRange (double from, double to, double step)
{
	while (from < to) {
		yield return from;
		from += step;
	}
}

public class ProbabilityBuilder : IMonadExpressionBuilder
{
	IMonad<T> IMonadExpressionBuilder.Bind<U, T> (IMonad<U> m, Func<U, IMonad<T>> f)
	{
		Probability<U> p = (Probability<U>)m;
		return new ComposedProbability<T> (t => {
			double probSum = 0;
			foreach (var u in SpaceOf<U> ()) {
				probSum += p.Prob (u) * ((Probability<T>)f (u)).Prob (t);
			}
			return probSum;
		});
	}

	IMonad<T> IMonadExpressionBuilder.Return<T> (T v)
		=> new ComposedProbability<T> (t => EqualityComparer<T>.Default.Equals (t, v) ? 1 : 0);

	IMonad<T> IMonadExpressionBuilder.Zero<T> () => new ComposedProbability<T> (_ => 1);

	IMonad<T> IMonadExpressionBuilder.Combine<T> (IMonad<T> m, IMonad<T> n) => throw new NotSupportedException ();
}

Now let's plot graphically the probability distribution of the resulting composition:

#r "OxyPlot"
using OxyPlot;

var result = CxRun<double, Probability<double>> (new ProbabilityBuilder (), async () => {
	var l = await new ContinuousDistributionProbability (new Beta (51, 151));
	var n = await new DiscreteDistributionProbability (new Binomial (l, 100));
	return n / 100.0;
});

var plotModel = new PlotModel {
	Title = "Empirical conversion rate",
	PlotType = PlotType.XY
};
var serie = new OxyPlot.Series.LinearBarSeries ();
serie.ItemsSource = new List<DataPoint> (DoubleRange (0, 1, 0.05).Select (i => new DataPoint (i, result.Prob (i))));
plotModel.Series.Add (serie);

plotModel;