il y a une fonctionnalité intéressante dans F#, c' est le pattern matching
Exemple:
let rec fib x =
match x with
| 1 -> 1
| 2 -> 1
| x -> fib(x - 1) + fib(x - 2) en C#
static int Fibonnaci(int x)
{
switch (x)
{
case 1 :
return 1;
case 2:
return 1;
default :
return Fibonnaci(x - 1) + Fibonnaci(x - 2);
}
} Dans cas c'est relativement simple, il suffit de remplacer "match x", par "switch(x)". Mais prenons un exemple un peu plus "dynamique" :
let (|ParseInt|_|) s =
let i = ref 0
if Int32.TryParse(s, i) then Some !i
else None
let (|ParseFloat|_|) s =
let i = ref 0.0
if Double.TryParse(s, NumberStyles.Any, CultureInfo.InvariantCulture, i) then Some !i
else None
let TryParse v =
match v with
| ParseInt i -> printfn " it's an int : %d" i
| ParseFloat f -> printfn " it's an double : %f" f
| _ -> printfn "unrecognized data"

ce qui est intérressant ici, c'est la possibilité de passer des paramètres entre la règle ("case") et le traitement de cette règle. Ce qui n'est plus possible avec un switch...
Revisitons le switch...
let rec fib x =
match x with
| 1 -> 1
| 2 -> 1
| x -> fib(x - 1) + fib(x - 2) 1,2,x sont des prédicats, et l'on pourrait écrire :
Predicate<int> one = x => x == 1;
Predicate<int> two = x => x == 2;
et le traitement associé est :
Func<int, int> onetwoAction = x1 => 1; il faut maintenant relier les règles à l'action, et en tant que développeur de la fonction de fibonnaci, j'ai envie d'écrire :
Func<int, int> Case1 = NewCase(one, onetwoAction );
Func<int, int> Case2 = NewCase(two, onetwoAction );
Func<int, int> defaultCase = x => Fib(x-1) + Fib(x-2);
Fib étant la fonction englobant Case1, Case2, defaultCase qui ressemble à :
int Fib(int arg)
{
if (one(arg))
return onetwoAction(arg);
else if (two(arg))
return onetwoAction(arg);
else
return defaultCase(arg);
}
mais evidemment tout cela c' est de la plomberie.... En regardant de plus près cela ressemble à une chaine de responsabilité (Chain-of-responsibility pattern).
il faut donc une classe qui possède l'action courante, et l'action suivante
class CaseHolderChain<T>
{
public T NextAction { get; set; }
public T Action { get; set; }
} La fonction 'NewCase' est donc :
public static Func<TArg, TResult> NewCase<TArg, TResult>(Predicate<TArg> test, Func<TArg, TResult> caseAction)
{
if (test == null)
throw new ArgumentNullException("test");
if (caseAction == null)
throw new ArgumentNullException("caseAction");
var thisCase = new CaseHolderChain<Func<TArg, TResult>>();
thisCase.Action = arg =>
{
if (test(arg))
return caseAction(arg);
return thisCase.NextAction(arg);
};
thisCase.NextAction = arg =>
{
return CaseException<TResult>(arg);
};
return thisCase.Action;
}
Ainsi la fonction retournée est une sorte de 'switch' avec une seule possibilité, en effet la fonction 'NextAction' est par défaut 'connectée' sur une méthode static soulevant une exception.
Il faut maintenant lier les 'cases' ensemble, c'est à dire remplacer NextAction du premier 'case' par le suivant, et ainsi de suite...
Mais pour cela il faut faire évoluer notre fonction 'NewCase', car il faut pouvoir changer la valeur de la propriété 'NextAction'.
class CaseHolderChainFun<TArg, TResult> : CaseHolderChain<Func<TArg, TResult>>
{
public TResult DoAction(TArg arg)
{
return Action(arg);
}
}
public static Func<TArg, TResult> NewCase<TArg, TResult>(Predicate<TArg> test, Func<TArg, TResult> caseAction)
{
if (test == null)
throw new ArgumentNullException("test");
if (caseAction == null)
throw new ArgumentNullException("caseAction");
var thisCase = new CaseHolderChainFun<TArg, TResult>();
thisCase.Action = arg =>
{
if (test(arg))
return caseAction(arg);
return thisCase.NextAction(arg);
};
thisCase.NextAction = arg =>
{
return CaseException<TResult>(arg);
};
return new Func<TArg, TResult>(thisCase.DoAction);
}
plutôt que de renvoyer une fonction pointant directement sur 'Action' (qui est une méthode statique), je renvoie une fonction pointant sur une méthode d'instance. Cela permet de retouver le type 'CaseHolderChainFun<>' facilement.
En effet 'Func<TArg, TResult>' est un type Delegate, qui possède une propriété 'Target', qui est différent de null si la fonction pointe sur une méthode d'instance, bingo !
relier plusieurs 'cases' devient un jeu d'enfant, c'est le rôle de la fonction MakeMatch :
static Func<TArg, TResult> MakeMatch<TArg, TResult>(Func<TArg, TResult> defaultCase, params Func<TArg, TResult>[] cases)
{
Func<TArg, TResult> Result;
if (defaultCase != null)
Result = arg => defaultCase(arg);
else
Result = arg => CaseException<TResult>(arg);
int i = -1;
cases.Reverse().ForEach((elem) =>
{
i++;
CaseHolderChainFun<TArg, TResult> holder = elem.Target as CaseHolderChainFun<TArg, TResult>;
if (holder == null)
throw new Exception(string.Format("{0}eme case is not supported, use NewCase function ", i));
if (Result != null)
{
holder.NextAction = Result;
}
Result = elem;
});
return Result;
}
Ici MakeMatch, réalise l'union de plusieurs règles entre elle, et en ajoutant à la fin de la chaîne l'action par défaut. Ici l'action par défaut est le premier paramètre, ceci afin de pouvoir utiliser 'params' pour la liste des règles. Maintenant la fonction de Fibonnaci ressemble a :
Func<int, int> fibo = null;
fibo = MakeMatch((int x) => fibo(x-1) + fibo(x-2),
NewCase((int x) => x == 1, x => 1),
NewCase((int x) => x == 2, x => 1));
Console.WriteLine(fibo(30));
Nous sommes bien loin du switch, mais celui-ci est 'dynamique'...
après un petit refactoring et 2 méthodes d'extensions plus tard, la fonction de fibonnaci peut s'écrire :
fibo = fibo.Default(x => fibo(x - 1) + fibo(x - 2))
.Case(x => x == 1, x => 1)
.Case(x => x == 2, x => 1); les méthodes d'extensions ressemble à :
private static TResult CaseException<TResult>(object value)
{
throw new Exception(string.Format("case not found for {0}", value));
}
public static Func<TArg, TResult> Case<TArg, TResult>(this Func<TArg, TResult> fn, Predicate<TArg> test, Func<TArg, TResult> caseAction)
{
var thisCase = new CaseHolderChainFun<TArg, TResult>();
thisCase.Action = arg =>
{
if (test(arg))
return caseAction(arg);
return thisCase.NextAction(arg);
};
if (fn != null)
thisCase.NextAction = fn;
else
thisCase.NextAction = arg =>
{
return CaseException<TResult>(arg);
};
return new Func<TArg, TResult>(thisCase.DoAction);
}
public static Func<TArg, TResult> Default<TArg, TResult>(this Func<TArg, TResult> fn, Func<TArg, TResult> aDefault)
{
var thisCase = new CaseHolderChainFun<TArg, TResult>();
if (aDefault != null)
thisCase.Action = aDefault;
else
thisCase.NextAction = arg =>
{
return CaseException<TResult>(arg);
};
return new Func<TArg, TResult>(thisCase.DoAction);
}
avec cette implémentation, il est impératif que '.Default' soit en premier, car il sera exécuté en dernier.
Maintenant que nous avons le principe de base, le 'TryParse' F# devrait ressembler à :
Action<object> TryParse = null;
TryParse = TryParse.Case((string str, ref int v) => Int32.TryParse(str, out v),
(str, v) => Console.WriteLine(string.Format("it 's an int -> {0}, {1}", str, v)))
.Case((string str, ref double v) => double.TryParse(str, NumberStyles.Any, CultureInfo.InvariantCulture, out v),
(str, v) => Console.WriteLine(string.Format("it 's an double -> {0}, {1}", str, v)));
TryParse("32");
TryParse("32.56");
TryParse(new OverflowException());
TryParse(new Exception());
et après execution
il faut remarquer ici, que le paramètre d'entrée est object, ce qui dans certains cas provoquera du boxing/unboxing.
l'implémentation de la méthode d'extension 'Case' est un peu différente.
public static Action<object> Case<TArg, TRef>(this Action<object> fn, Predicate<TArg, TRef> test, Action<TArg, TRef> caseAction)
{
var thisCase = new CaseActionHolder<object>();
thisCase.Action = (arg) =>
{
if (typeof(TArg).IsAssignableFrom(arg.GetType()))
{
var convertedArg = (TArg)arg;
var r = default(TRef);
if (test(convertedArg, ref r))
{
caseAction(convertedArg, r);
return;
}
}
thisCase.NextAction(arg);
};
//no exception by default
thisCase.NextAction = (arg) => { };
return new Action<object>(thisCase.DoAction);
}
public static Action<object> Default(this Action<object> fn, Action<object> aDefault)
{
var thisCase = new CaseActionHolder<object>();
if (aDefault != null)
thisCase.Action = aDefault;
else
thisCase.Action = arg => { };
return new Action<object>(thisCase.DoAction);
}
Conclusion :
Les expressions lambda et les méthodes d'extension nous autorise à toucher du doigt la programmation fonctionnelle. Mais dans le cas présent, F# vérifie les règles qui ne "match" pas à la compilation, ce qui n'est pas le cas ici. De plus l'écriture est ici encore plus verbeuse que celle de F#.
Matthew Podwysocki à implémenté d'autres aspects fonctionnel que l'on retrouve dans F# (ou d'autre langage fonctionnel), tel Fold/ unfold, Map, Map2, ... , son projet se touve sur : http://code.msdn.microsoft.com/FunctionalCSharp