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
duck<t>()
system FunctionSoftInterface
Aspect