Duck Typing

The Elements compiler includes explicit support for Duck Typing, for all languages.

The name "duck typing" comes from the old saying that if something walks like a duck and quacks like a duck, it is a duck – and applies the same concept to objects. In essence, it means that if an object has all the methods or properties required by a specific interface, with Duck Typing you can treat it as if it implemented that interface (even if it does not).

Imagine we have the following (a bit contrived) types declared, and let's further assume that some of them are outside of our direct control – maybe they are declared in the core framework, or in a piece of the project we don't want to touch:

type
  IFooBar = interface
    method DoFoo;
    method DoBar;
  end;

  Foo = class
    method DoFoo;
  end;

  Bar = class
    method DoBar;
  end;

  FooBar = class
    method DoFoo;
    method DoBar;
  end;
public interface IFooBar
{
    void DoFoo();
    void DoBar();
}

public class Foo
{
    void DoFoo() {}
}

public class Bar
{
    void DoBar() {}
}

public class FooBar
{
    void DoFoo() {}
    void DoBar() {}
}
public interface IFooBar {
    func DoFoo()
    func DoBar()
}

public class Foo {
    func DoFoo() {}
}

public class Bar {
    func DoBar() {}
}

public class FooBar {
    func DoFoo() {}
    func DoBar() {}
}
public interface IFooBar
{
    void DoFoo();
    void DoBar();
}

public class Foo
{
    void DoFoo() {}
}

public class Bar
{
    void DoBar() {}
}

public class FooBar
{
    void DoFoo() {}
    void DoBar() {}
}
Public Interface IFooBar
  Sub DoFoo()
  Sub method DoBar()
End Interface

Public Class Foo
  Sub DoFoo()
End Class

Public Class Bar
  Sub DoBar()
End Class

Public Class FooBar
  Sub DoFoo()
  Sub DoBar()
End Class

As you see, we have an interface IFooBar that declares a couple of methods. We also have three classes that look pretty similar but with one caveat: while they do implement some of the same methods defined by IFooBar, they don't actually implement the IFooBar interface itself. This means that if we now have a method like this:

method Test(o: IFooBar);
void Test(IFooBar o);
func Test(_ o: IFooBar)
void Test(IFooBar o);
Sub Test(o As IFooBar)

we cannot actually pass a FooBar instance to it, even though FooBar obviously implements all the necessary methods. There's nothing that our Test method could throw at the FooBar instance that it could not handle, yet we can't just pass it in.

Enter Duck Typing

Starting with Elements 5.0, the generic duck<t>() system function allows you to apply duck typing to let the compiler "convert" a FooBar into an IFooBar, where necessary. For example, you could write:

var fb := new FooBar;
Test(duck<IFooBar>(fb));
var fb = new FooBar();
Test(duck<IFooBar>(fb));
let fb = FooBar()
Test(duck<IFooBar>(fb))
var fb = new FooBar();
Test(duck<IFooBar>(fb));
Dim fb = New FooBar()
Test(duck(Of IFooBar)(fb))

and pass the object in. The result of duck() is, essentially, an IFooBar, and you can use it in any context that accepts an IFooBar – method calls, variable assignments, you name it. This works because FooBar implements all the necessary methods to satisfy an IFooBar implementation – and the compiler takes care of the rest.

So what if that is not the case? What would happen if instead we write the following?

var fb := new Foo;
Test(duck<IFooBar>(fb));
var fb = new Foo();
Test(duck<IFooBar>(fb));
let fb = Foo()
Test(duck<IFooBar>(fb))
var fb = new Foo();
Test(duck<IFooBar>(fb));
Dim fb = New Foo()
Test(duck(Of IFooBar)(fb))

where, as you note above, Foo does implement DoFoo, but does not implement DoBar, which is required for the interface. So clearly, Foo doesn't qualify to be duck typed as an IFooBar? That's correct, and in fact the line above would fail, with an error such as:

  • (E265) Static duck typing failed because of missing methods
  • (N2) Matching method "MyApplication.IFooBar.DoBar" is missing

But what if you're fully aware your object only satisfies a subset of the interface, and you want to pass it anyway? Maybe you know that Test only makes use of DoFoo and does not need DoBar?

Elements' duck typing has a solution for this as well, by passing an optional DuckTypingMode enum value to the duck() function. DuckTypingMode has three values; the default is Static*, and we've seen it in action above. Static duck typing will enforce that the passed object fully qualifies for the interface, and will fail with a compiler error if any member (method, property or event) of the interface is not provided by the type.

The second DuckTypingMode is Weak. In weak mode, the compiler will match any interface members it can find, just like in static mode. But for any member it does not find on the original type, it will generate a stub that throws a "Not Implemented" exception. This enables us to write:

var fb := new Foo;
Test(duck<IFooBar>(fb, DuckTypingMode.Weak));
var fb = new Foo();
Test(duck<IFooBar>(fb, DuckTypingMode.Weak));
let fb = Foo()
Test(duck<IFooBar>(fb, DuckTypingMode.Weak))
var fb = new Foo();
Test(duck<IFooBar>(fb, DuckTypingMode.Weak));
Dim fb = New Foo()
Test(duck(Of IFooBar)(fb, DuckTypingMode.Weak))

and successfully pass a Foo object to Test(). As long as Test only calls DoFoo, everything will be fine and work as expected; if Test were to call DoBar as well, an exception would be thrown at runtime.

The third and final DuckTypingMode is Dynamic. Dynamic duck typing will not directly map methods of the source object to the interface; instead, it will create a wrapper class that will dynamically call the interface members, based on what is available at runtime.

You can think of these three modes of duck-typing as being on a scale, with Static (the default) being 100% type safe. If static duck typing compiles, you can rest assured that everything will work as you expect, at runtime. Weak mode trades some type safety for a model that is weaker typed, comparable to, for example, Objective-C's id type (which essentially does weak duck typing everywhere by default – if an object has a method of a given name, you can call it). Dynamic is at the opposite end of the scale, completely resolving all calls at runtime, more like true dynamic languages such as JavaScript.

Soft Interfaces

So this is all good and well, but imagine you have a large (and untouchable) library with classes that implement the DoFoo/DoBar pattern, and you plan to use those all over your code base. Sure, you can declare IFooBar, and use the duck method to duck-type those objects all over the place, but that will get annoying quickly. The compiler knows that DoFoo and DoBar methods are enough to satisfy the interface, so wouldn't it be great if you could let the compiler worry about the duck typing where necessary?

That's where soft interfaces come in. Instead of declaring IFooBar as above, you could declare it as a Soft Interface, as follows:

type
  IFooBar = soft interface
    method DoFoo;
    method DoBar;
  end;
[SoftInterface]
public interface IFooBar
{
    void DoFoo();
    void DoBar();
}
@SoftInterface
public interface IFooBar {
    func DoFoo()
    func DoBar()
}
@SoftInterface
public interface IFooBar
{
    void DoFoo();
    void DoBar();
}
<SoftInterface>
Public Interface IFooBar
  Sub DoFoo()
  Sub method DoBar()
End Interface

Simply adding the soft keyword (in Oxygene) or the SoftInterface aspect (all languages) lets the complier know that this interface represents a pattern it will find in classes that do not actually implement the interface themselves. As a result, you can now simply declare the Test method as before:

method Test(o: IFooBar);
void Test(IFooBar o);
func Test(_ o: IFooBar)
void Test(IFooBar o);
Sub Test(o As IFooBar)

And just pass your FooBar instances to it – no call to duck() necessary.

var fb := new Foo;
Test(fb);
var fb = new Foo();
Test(fb);
let fb = Foo()
Test(fb)
var fb = new Foo();
Test(fb);
Dim fb = New Foo()
Test(fb)

In essence, the compiler will treat any class that implements the matching methods – DoFoo and DoBar in this case – as actually implementing the interface. This works even for classes imported from external frameworks.

To give a more concrete sample based on real life objects, imagine the following scenario:

type
  INumberToStringFormatter = soft interface
    method ToString(aFormat: String): String;
  end;

var d: Double := 15.2;
var x: INumberToStringFormatter := d; // no cast necessary
writeLn(x.ToString('m'));
[SoftInterface]
public interface INumberToStringFormatter
{
    void string ToString(string format);
}

double d = 15.2;
INumberToStringFormatter x = d; // no cast necessary
writeLn(x.ToString("m"));
@SoftInterface
public interface INumberToStringFormatter {
    func ToString(_ format: String) -> String
}

let d: Double = 15.2;
let x: INumberToStringFormatter = d; // no cast necessary
writeLn(x.ToString("m"));
@SoftInterface
public interface INumberToStringFormatter
{
    void string ToString(string format);
}

double d = 15.2;
INumberToStringFormatter x = d; // no cast necessary
writeLn(x.ToString("m"));
<Soft>
Public Interface INumberToStringFormatter
  Sub ToString(format As String) As String
End Interface

Dim d As Double = 15.2
Dim x As INumberToStringFormatter = d // no cast necessary
writeLn(x.ToString("m"))

Different to the regular ToString() method, not every object in .NET implements ToString(String). Yet with the soft interface declared here, you now have a common type that you could assign a Double, an Int32 or even a Guid to – and call ToString(String) on. All with complete type safety.

See Also