Skip to content

Commit

Permalink
Add a section on overloads vs. union/optional (#426)
Browse files Browse the repository at this point in the history
Fixes #307.
  • Loading branch information
TimothyGu authored and tobie committed Sep 22, 2017
1 parent 12add43 commit a6a713f
Showing 1 changed file with 244 additions and 0 deletions.
244 changes: 244 additions & 0 deletions index.bs
Original file line number Diff line number Diff line change
Expand Up @@ -3255,6 +3255,250 @@ be the same.
</div>


<h5 id="idl-overloading-vs-union">Overloading vs. union types</h5>

<i>This section is informative.</i>

<div class="non-normative">

For specifications defining IDL [=operations=], it might seem that [=overloaded|overloads=] and a
combination of [=union types=] and [=optional arguments=] have some feature overlap.

It is first important to note that [=overloaded|overloads=] have different behaviors than [=union
types=] or [=optional arguments=], and one <em>cannot</em> be fully defined using the other (unless,
of course, additional prose is provided, which can defeat the purpose of the Web IDL type system).
For example, consider the {{CanvasDrawPath/stroke()}} operations defined on the {{CanvasDrawPath}}
interface [[HTML]]:

<pre highlight="webidl">
interface CanvasDrawPathExcerpt {
void stroke();
void stroke(Path2D path);
};
</pre>

Per the ECMAScript language binding, calling <code>stroke(undefined)</code> on an object
implementing <code class="idl">CanvasDrawPathExcerpt</code> would attempt to call the second
overload, yielding a {{TypeError}} since <emu-val>undefined</emu-val> cannot be <a
href="#es-to-interface">converted</a> to a {{Path2D}}. However, if the operations were instead
defined with [=optional arguments=] and merged into one,

<pre highlight="webidl">
interface CanvasDrawPathExcerptOptional {
void stroke(optional Path2D path);
};
</pre>

the [=overload resolution algorithm=] would treat the <var ignore>path</var> argument as [=not
present=] given the same call <code>stroke(undefined)</code>, and not throw any exceptions.

Note: For this particular example, the latter behavior is actually what Web developers would
generally expect. If {{CanvasDrawPath}} were to be designed today, [=optional arguments=] would be
used for <code class="idl">stroke()</code>.

Additionally, there are semantic differences as well. [=Union types=] are usually used in the sense
that "any of the types would work in about the same way". In contrast, [=overloaded=] operations are
designed to map well to language features such as C++ overloading, and are usually a better fit for
operations with more substantial differences in what they do given arguments of different types.
However, in most cases, operations with such substantial differences are best off with different
names to avoid confusion for Web developers, since the ECMAScript language does not provide
language-level overloading. As such, overloads are rarely appropriate for new APIs, instead often
appearing in legacy APIs or in specialized circumstances.

That being said, we offer the following recommendations and examples in case of difficulties to
determine what Web IDL language feature to use:

* In the unusual case where the operation needs to return values of different types for different
argument types, [=overloaded|overloading=] will result in more expressive IDL fragments. <span
class="note">This is almost never appropriate API design, and separate operations with distinct
names usually are a better choice for such cases.</span>

Suppose there is an operation <code class="idl">calculate()</code> that accepts a {{long}},
{{DOMString}}, or <code class="idl">CalculatableInterface</code> (an [=interface type=]) as its
only argument, and returns a value of the same type as its argument. It would be clearer to
write the IDL fragment using [=overloaded=] operations as

<pre highlight="webidl">
interface A {
long calculate(long input);
DOMString calculate(DOMString input);
CalculatableInterface calculate(CalculatableInterface input);
};
</pre>

than using a [=union type=] with a [=typedef=] as

<pre highlight="webidl">
typedef (long or DOMString or CalculatableInterface) Calculatable;
interface A {
Calculatable calculate(Calculatable input);
};
</pre>

which does not convey the fact that the return value is always of the same type as <var
ignore>input</var>.

The problem is exacerbated when one of the overloads has a return type of {{void}}, since
[=union types=] cannot even contain {{void}} as a [=member type=]. In that case, a return type
of {{any}} needs to be used with appropriate prose defining the return type, further decreasing
expressiveness.

If the specified <code class="idl">calculate()</code> is a new API and does not have any
compatibility concerns, it is suggested to use different names for the overloaded operations,
perhaps as

<pre highlight="webidl">
interface A {
long calculateNumber(long input);
DOMString calculateString(DOMString input);
CalculatableInterface calculateCalculatableInterface(CalculatableInterface input);
};
</pre>

which allows Web developers to write explicit and unambiguous code.

* When the operation has significantly different semantics for different argument types or
lengths, [=overloaded|overloading=] is preferred. Again, in such scenarios, it is usually better
to create separate operations with distinct names, but legacy APIs sometimes follow this
pattern.

As an example, the {{CSS/supports()}} operations of the {{CSS}} interface is defined as the
following IDL fragment [[CSS3-CONDITIONAL]] [[CSSOM]].

<pre highlight="webidl">
partial interface CSS {
static boolean supports(CSSOMString property, CSSOMString value);
static boolean supports(CSSOMString conditionText);
};
</pre>

Using [=optional arguments=] one can rewrite the IDL fragment as follows:

<pre highlight="webidl">
partial interface CSSExcerptOptional {
static boolean supports(CSSOMString propertyOrConditionText, optional CSSOMString value);
};
</pre>

Even though the IDL is shorter in the second version, two distinctively different concepts are
conflated in the first argument. Without [=overloaded|overloads=], the question "is <var
ignore>property</var> or <var ignore>conditionText</var> paired with <var ignore>value</var>?"
is much more difficult to answer without reading the prose definition of the operation. This
makes the second version remarkably less readable than the first.

Another consideration is that the prose for [=overloaded=] operations can be specified in
separate blocks, which can aid in both reading and writing specifications. This is not the case
for [=optional arguments=]. This means that in the first case the specification author can write
the prose definition of the operations as:

<div algorithm="execute supports() with overloads">

The <code class="idl">supports(<var ignore>property</var>, <var ignore>value</var>)</code>
method, when called, must run these steps:

1. …

----

The <code class="idl">supports(<var ignore>conditionText</var>)</code> method, when called,
must run these steps:

1. …

</div>

Yet using <var ignore>value</var> as an [=optional argument=], the specification author has to
use more boilerplate-style text to effectively replicate the [=overload resolution algorithm=].

<div algorithm="execute supports() with optional argument">

The <code class="idl">supports(|propertyOrConditionText|, |value|)</code> method, when
called, must run these steps:

1. If |value| is given, then:
1. Let <var ignore>property</var> be |propertyOrConditionText|.
1. …
1. Otherwise:
1. Let <var ignore>conditionText</var> be |propertyOrConditionText|.
1. …

</div>

If the two overloads have little to no shared parts, it is better to leave overload resolution
to the IDL mechanism.

* If the operation accepts multiple types for multiple arguments with no coupling between types of
different arguments, [=union types=] can sometimes be the only viable solution.

<pre highlight="webidl">
typedef (long long or DOMString or CalculatableInterface) SupportedArgument;
interface A {
void add(SupportedArgument operand1, SupportedArgument operand2);
};
</pre>

For the <code class="idl">add()</code> operation above, to specify it using
[=overloaded|overloads=] would require

<pre highlight="webidl">
interface A {
void add(long long operand1, long long operand2);
void add(long long operand1, DOMString operand2);
void add(long long operand1, CalculatableInterface operand2);
void add(DOMString operand1, long long operand2);
void add(DOMString operand1, DOMString operand2);
void add(DOMString operand1, CalculatableInterface operand2);
void add(CalculatableInterface operand1, long long operand2);
void add(CalculatableInterface operand1, DOMString operand2);
void add(CalculatableInterface operand1, CalculatableInterface operand2);
};
</pre>

and nine times the corresponding prose!

* Specification authors are encouraged to treat missing argument and <emu-val>undefined</emu-val>
argument the same way in the ECMAScript language binding.

Given the following IDL fragment:

<pre highlight="webidl">
interface A {
void foo();
void foo(Node? arg);
};
</pre>

Using the ECMAScript language binding, calling <code>foo(undefined)</code> and
<code>foo(null)</code> would both run the steps corresponding to the <code
class="idl">foo(|arg|)</code> operation, with |arg| set to null, while <code>foo()</code> alone
would go to the first overload. This can be a surprising behavior for many API users. Instead,
specification authors are encouraged to use an [=optional argument=], which would categorize
both <code>foo()</code> and <code>foo(undefined)</code> as "|arg| is [=not
present=]".

<pre highlight="webidl">
interface A {
void foo(optional Node? arg);
};
</pre>

In general, optionality is best expressed using the <emu-t>optional</emu-t> keyword, and not
using overloads.

When the case fits none of the categories above, it is up to the specification author to choose the
style, since it is most likely that either style would sufficiently and conveniently describe the
intended behavior. However, the definition and <a href="#es-to-union">conversion algorithms</a> of
[=union types=] and [=optional arguments=] are simpler to implement and reason about than [=overload
resolution algorithm|those=] of [=overloaded|overloads=], and usually result in more idiomatic APIs
in the ECMAScript language binding. Thus, unless any other considerations apply, [=union types=]
(and/or [=optional arguments=]) are the default choice.

Specifications are also free to mix and match union types and overloads, if the author finds it
appropriate and convenient.

</div>


<h4 id="idl-iterable">Iterable declarations</h4>

An [=interface=] can be declared to be
Expand Down

0 comments on commit a6a713f

Please sign in to comment.