I recently read a really interesting post on adding functional pattern matching to C#. Here's a quick and dirty implementation using delegates and dictionaries.
Right now, I require that Match
return an object of type TReturn
. Is there any way I can easily extend my current implementation to be void
(i.e. Match
is not required to return anything) without significant code duplication? When I first tried this, I ended up essentially copying Match
but omitting the second type parameter.
/// <summary>
/// Represents a switch on TIn.
/// </summary>
public class Match<TIn, TReturn>
{
private Dictionary<Type, Delegate> funcs =
new Dictionary<Type, Delegate>();
private Dictionary<TIn, TReturn> values =
new Dictionary<TIn, TReturn>();
private TReturn _else;
/// <summary>
/// Adds a case, action pair
/// </summary>
public void Case<T>(Func<T, TReturn> func)
where T : TIn
{
funcs[typeof(T)] = func;
}
/// <summary>
/// Adds a case, value pair
/// </summary>
public void Case(TIn x, TReturn value)
{
values[x] = value;
}
/// <summary>
/// Adds the default case value
/// </summary>
public void Else(TIn x, TReturn value)
{
_else = value;
}
/// <summary>
/// Evaluates the match on a query
/// Returns an object of type TReturn
/// for first successful match
/// </summary>
public TReturn Eval<T>(T query)
where T : TIn
{
TReturn value;
if (values.TryGetValue(query, out value))
return value;
Delegate func;
if (funcs.TryGetValue(query.GetType(), out func))
return (TReturn)func.DynamicInvoke(query);
return _else;
}
}
And here's a simple test. One thing I was wondering: is it inefficient to declare a new Match
inside the Calculate
method?
class Program
{
public static double Calculate(Expression exp)
{
var match = new Match<Expression, double>();
match.Case<Num>((x) => x.Value);
match.Case<Mul>((x) => Calculate(x.Left) * Calculate(x.Right));
match.Case<Add>((x) => Calculate(x.Left) + Calculate(x.Right));
return match.Eval(exp);
}
public static void Main(string[] args)
{
Expression tree = new Add(new Add(new Num(1), new Num(2)), new Mul(new Num(3), new Num(4)));
double x = Calculate(tree) // = 15
}
}
abstract class Expression
{
public double Value;
}
abstract class BinaryExpression : Expression
{
public Expression Left;
public Expression Right;
public BinaryExpression(Expression left, Expression right)
{
Left = left; Right = right;
}
}
class Add : BinaryExpression
{
public Add(Expression left, Expression right)
: base(left, right)
{
}
}
class Mul : BinaryExpression
{
public Mul(Expression left, Expression right)
: base(left, right)
{
}
}
class Num : Expression
{
public Num(double x)
{
Value = x;
}
}
1 Answer 1
In your Match
class, I would mark the two private dictionaries as readonly
to signify intent and keep them from being assigned to accidentally anywhere else:
private readonly Dictionary<Type, Delegate> funcs =
new Dictionary<Type, Delegate>();
private readonly Dictionary<TIn, TReturn> values =
new Dictionary<TIn, TReturn>();
Secondly, I'd design to interfaces and make them IDictionaries
as good OO practice:
private readonly IDictionary<Type, Delegate> funcs =
new Dictionary<Type, Delegate>();
private readonly IDictionary<TIn, TReturn> values =
new Dictionary<TIn, TReturn>();
In your example classes, you should forego public
member data and use properties accessing backing fields (also marked readonly
):
abstract class Expression
{
private readonly double _Value;
protected Expression(double value)
{
this._Value = value;
}
public double Value
{
get
{
return this._Value;
}
}
}
abstract class BinaryExpression : Expression
{
private readonly Expression _Left;
private readonly Expression _Right;
public BinaryExpression(Expression left, Expression right) : base(default(double))
{
this._Left = left;
this._Right = right;
}
public Expression Left
{
get
{
return this._Left;
}
}
public Expression Right
{
get
{
return this._Right;
}
}
}
class Add : BinaryExpression
{
public Add(Expression left, Expression right) : base(left, right)
{
}
}
class Mul : BinaryExpression
{
public Mul(Expression left, Expression right) : base(left, right)
{
}
}
class Num : Expression
{
public Num(double x) : base(x)
{
}
}
-
1\$\begingroup\$ I usually make my fields
public readonly
. Thanks for the suggestions! \$\endgroup\$rookie– rookie2015年09月30日 21:27:01 +00:00Commented Sep 30, 2015 at 21:27
Else
; it should only take aTReturn
, not aTIn
. My bad! \$\endgroup\$