blob: 3b8503b5a92c83ca30a07a20a7cabeb1da04df0c [file] [log] [blame]
:orphan:
=================================================
Rationales for the Swift standard library designs
=================================================
This document collects rationales for the Swift standard library. It is not
meant to document all possible designs that we considered, but might describe
some of those, when important to explain the design that was chosen.
Current designs
===============
Some ``NSString`` APIs are mirrored on ``String``
-------------------------------------------------
There was not enough time in Swift 1.0 to design a rich ``String`` API, so we
reimplemented most of ``NSString`` APIs on ``String`` for parity. This brought
the exact ``NSString`` semantics of those APIs, for example, treatment of
Unicode or behavior in edge cases (for example, empty strings), which we might
want to reconsider.
Radars: rdar://problem/19705854
``size_t`` is unsigned, but it is imported as ``Int``
-----------------------------------------------------
Converging APIs to use ``Int`` as the default integer type allows users to
write fewer explicit type conversions.
Importing ``size_t`` as a signed ``Int`` type would not be a problem for 64-bit
platforms. The only concern is about 32-bit platforms, and only about
operating on array-like data structures that span more than half of the address
space. Even today, in 2015, there are enough 32-bit platforms that are still
interesting, and x32 ABIs for 64-bit CPUs are also important. We agree that
32-bit platforms are important, but the usecase for an unsigned ``size_t`` on
32-bit platforms is pretty marginal, and for code that nevertheless needs to do
that there is always the option of doing a bitcast to ``UInt`` or using C.
Type Conversions
----------------
The canonical way to convert from an instance `x` of type ``T`` to
type ``U`` is ``U(x)``, a precedent set by ``Int(value: UInt32)``.
Conversions that can fail should use failable initializers,
e.g. ``Int(text: String)``, yielding a ``Int?``. When other forms provide
added convenience, they may be provided as well. For example::
String.Index(s.utf16.startIndex.successor(), within: s) // canonical
s.utf16.startIndex.successor().samePosition(in: s) // alternate
Converting initializers generally take one parameter. A converting
initializer's first parameter should not have an argument label unless
it indicates a lossy, non-typesafe, or non-standard conversion method,
e.g. ``Int(bitPattern: someUInt)``. When a converting initializer
requires a parameter for context, it should not come first, and
generally *should* use a keyword. For example, ``String(33, radix:
2)``.
:Rationale: First, type conversions are typical trouble spots, and we
like the idea that people are explicit about the types to which
they're converting. Secondly, avoiding method or property syntax
provides a distinct context for code completion. Rather than
appearing in completions offered after ``.``, for example, the
available conversions could show up whenever the user hit the "tab"
key after an expression.
Protocols with restricted conformance rules
-------------------------------------------
It is sometimes useful to define a public protocol that only a limited set of
types can adopt. There is no language feature in Swift to disallow declaring
conformances in third-party code: as long as the requirements are implemented
and the protocol is accessible, the compiler allows the conformance.
The standard library adopts the following pattern: the protocol is declared as
a regular public type, but it includes at least one requirement named using the
underscore rule. That underscored API becomes private to the users according
to the standard library convention, effectively preventing third-party code from
declaring a conformance.
For example::
public protocol CVarArgType {
var _cVarArgEncoding: [Word] { get }
}
// Public API that uses CVaListPointer, so CVarArgType has to be public, too.
public func withVaList<R>(
_ args: [CVarArgType],
@noescape invoke body: (CVaListPointer) -> R
) -> R
High-order functions on collections return ``Array``\ s
-------------------------------------------------------
We can't make ``map()``, ``filter()``, etc. all return ``Self``:
- ``map()`` takes a function ``(T) -> U`` and therefore can't return Self
literally. The required language feature for making ``map()`` return
something like ``Self`` in generic code (higher-kinded types) doesn't exist
in Swift. You can't write a method like ``func map(_ f: (T) -> U) -> Self<U>``
today.
- There are lots of sequences that don't have an appropriate form for the
result. What happens when you filter the only element out of a
``SequenceOfOne<T>``, which is defined to have exactly one element?
- A ``map()`` that returns ``Self<U>`` hews most closely to the signature
required by Functor (mathematical purity of signature), but if you make map
on ``Set`` or ``Dictionary`` return ``Self``, it violates the semantic laws
required by Functor, so it's a false purity. We'd rather preserve the
semantics of functional ``map()`` than its signature.
- The behavior is surprising (and error-prone) in generic code::
func countFlattenedElements<
S : SequenceType where S.Generator.Element == Set<Double>
>(_ sequence: S) -> Int {
return sequence.map { $0.count }.reduce(0) { $0 + $1 }
}
The function behaves as expected when given an ``[Set<Double>]``, but the
results are wrong for ``Set<Set<Double>>``. The ``sequence.map()`` operation
would return a ``Set<Int>``, and all non-unique counts would disappear.
- Even if we throw semantics under the bus, maintaining mathematical purity of
signature prevents us from providing useful variants of these algorithms that
are the same in spirit, like the ``flatMap()`` that selects the non-nil
elements of the result sequence.
The `remove*()` method family on collections
--------------------------------------------
Protocol extensions for ``RangeReplaceableCollectionType`` define
``removeFirst(n: Int)`` and ``removeLast(n: Int)``. These functions remove
exactly ``n`` elements; they don't clamp ``n`` to ``count`` or they could be
masking bugs.
Since the standard library tries to preserve information, it also defines
special overloads that return just one element, ``removeFirst() -> Element``
and ``removeLast() -> Element``, that return the removed element. These
overloads have a precondition that the collection is not empty. Another
possible design would be that they don't have preconditions and return
``Element?``. Doing so would make the overload set inconsistent: semantics of
different overloads would be significantly different. It would be surprising
that ``myData.removeFirst()`` and ``myData.removeFirst(1)`` are not equivalent.
Lazy functions that operate on sequences and collections
--------------------------------------------------------
In many cases functions that operate on sequences can be implemented either
lazily or eagerly without compromising performance. To decide between a lazy
and an eager implementation, the standard library uses the following rule.
When there is a choice, and not explicitly required by the API semantics,
functions don't return lazy collection wrappers that refer to users' closures.
The consequence is that all users' closures are ``@noescape``, except in an
explicitly lazy context.
Based on this rule, we conclude that ``enumerate()``, ``zip()`` and
``reverse()`` return lazy wrappers, but ``filter()`` and ``map()`` don't. For
the first three functions being lazy is the right default, since usually the
result is immediately consumed by for-in, so we don't want to allocate memory
for it.
Note that neither of the two ``sorted()`` methods (neither one that accepts a
custom comparator closure, nor one that uses the ``Comparable`` conformance)
can't be lazy, because the lazy version would be less efficient than the eager
one.
A different design that was rejected is to preserve consistency with other
strict functions by making these methods strict, but then client code needs to
call an API with a different name, say ``lazyEnumerate()`` to opt into
laziness. The problem is that the eager API, which would have a shorter and
less obscure name, would be less efficient for the common case.
Use of ``BooleanType`` in library APIs
--------------------------------------
Use ``Bool`` instead of a generic function over a ``BooleanType``, unless there
are special circumstances (for example, ``func &&`` is designed to work on all
boolean values so that ``&&`` feels like a part of the language).
``BooleanType`` is a protocol to which only ``Bool`` and ``ObjCBool`` conform.
Users don't usually interact ``ObjCBool`` instances, except when using certain
specific APIs (for example, APIs that operate on pointers to ``BOOL``). If
someone already has an ``ObjCBool`` instance for whatever strange reason, they
can just convert it to ``Bool``. We think this is the right tradeoff:
simplifying function signatures is more important than making a marginal
usecase a bit more convenient.
Possible future directions
==========================
This section describes some of the possible future designs that we have
discussed. Some might get dismissed, others might become full proposals and
get implemented.
Mixed-type fixed-point arithmetic
---------------------------------
Radars: rdar://problem/18812545 rdar://problem/18812365
Standard library only defines arithmetic operators for LHS and RHS that have
matching types. It might be useful to allow users to mix types.
There are multiple approaches:
* AIR model,
* overloads in the standard library for operations that are always safe and
can't trap (e.g., comparisons),
* overloads in the standard library for all operations.
TODO: describe advantages
The arguments towards not doing any of these, at least in the short term:
* demand might be lower than we think: seems like users have converged towards
using ``Int`` as the default integer type.
* mitigation: import good C APIs that use appropriate typedefs for
unsigned integers (``size_t`` for example) as ``Int``.
Swift: Power operator
---------------------
Radars: rdar://problem/17283778
It would be very useful to have a power operator in Swift. We want to make
code look as close as possible to the domain notation, the two-dimensional
formula in this case. In the two-dimensional representation exponentiation is
represented by a change in formatting. With ``pow()``, once you see the comma,
you have to scan to the left and count parentheses to even understand that
there is a ``pow()`` there.
The biggest concern is that adding an operator has a high barrier.
Nevertheless, we agree ``**`` is the right way to spell it, if we were to have
it. Also there was some agreement that if we did not put this operator in the
core library (so that you won't get it by default), it would become much more
compelling.
We will revisit the discussion when we have submodules for the standard
library, in one form or the other.