Certaines personnes (développeurs Ruby, Python...) aiment le duck typing. Pour ma part, je préfère le typage statique, fort et inféré, même si le typage dynamique a parfois ses avantages. En F#, il est possible d'obtenir du duck typing: il suffit d'utiliser l'introspection. Voici une petite classe pour d'exemple :

open System.Reflection
 
type Duck(value: obj) =
    let mutable value = value
    let mutable ty = value.GetType()
    member d.Type = ty
    member d.Cast<'a>() = unbox<'a> value
 
    member d.Exec(str, args) = 
        Duck (ty.InvokeMember(str, BindingFlags.GetProperty ||| BindingFlags.InvokeMethod, null, value, args))
 
    member d.X(str) =
        d.Exec(str, [||])
 
    member d.X(str, arg1) =
        d.Exec(str, [|box arg1|])
 
    member d.X(str, arg1, arg2) =
        d.Exec(str, [|box arg1; box arg2|])
 
    member d.Item with get str = d.Exec(str, [||])
    member d.Val with get() = value
                 and  set (x:obj) = value <- x; ty <- value.GetType()

Et voici un exemple d'utilisation :

let a = Duck 42
 
a.Val <- "test"          // modifie a: il contient maintenant une chaine
a.["Length"]             // renvoie 4
a.X("Contains", "st")    // exécute une méthode - renvoie true
 
let b = Duck [1; 2; 3]
b.Val <- b.["Length"]
 
b.Type.ToString()        // renvoie "System.Int32"
printfn "%d" (b.Cast())  // affiche 3 

La méthode Cast est utilisée pour obtenir une valeur typée. b.Cast<int>() essaie de renvoyer un int (la conversion est testée à l'exécution). L'annotation de type n'est pas toujours nécessaire, du fait de l'inférence de type.

> let length d : int = (Duck d).["Length"].Cast();;
 
val length : 'a -> int
 
> length "test";;
val it : int = 4
> length [1; 2];;
val it : int = 2

Bien sûr, tout ce code n'est pas sûr : les noms des méthodes peuvent être calculés à l'exécution, peuvent provenir d'entrées utilisateur ou encore contenir des fautes de frappe. Dès que l'on appelle la méthode Cast, on revient dans le monde sûr et statiquement vérifié de F#.