ch19.md 6.9 KB

Chapter 19. Overload resolution

In a call p(args) the routine p that matches best is selected. If multiple routines match equally well, the ambiguity is reported during semantic analysis. Every arg in args needs to match. Let f be the formal parameter’s type and a the type of the argument. There are multiple different categories how an argument can match:

  1. Exact match: a and f are of the same type.
  2. Literal match: a is an integer literal of value v and f is a signed or unsigned integer type and v is in f's range. Or: a is a floating-point literal of value v and f is a floating-point type and v is in f's range.
  3. Generic match: f is a generic type and a matches, for instance a is int and f is a generic (constrained) parameter type (like in [T] or [T: int|char]).
  4. Subrange or subtype match: a is a range[T] and T matches f exactly. Or: a is a subtype of f.
  5. Integral conversion match: a is convertible to f and f and a is some integer or floating-point type.
  6. Conversion match: a is convertible to f, possibly via a user defined converter. These matching categories have a priority: An exact match is better than a literal match and that is better than a generic match etc. In the following, count(p, m) counts the number of matches of the matching category m for the routine p. A routine p matches better than a routine q if the following algorithm returns true:

                                                                           119
    

    for each matching category m in ["exact match", "literal match",

                               "generic match", "subtype match",
                               "integral match", "conversion match"]:
    

    if count(p, m) > count(q, m): return true elif count(p, m) == count(q, m): discard "continue with next category m" else: return false return "ambiguous" Some examples: proc takesInt(x: int) = echo "int" proc takesIntT = echo "T" proc takesInt(x: int16) = echo "int16" takesInt(4) # "int" var x: int32 takesInt(x) # "T" var y: int16 takesInt(y) # "int16" var z: range[0..4] = 0 takesInt(z) # "T" If this algorithm returns “ambiguous” further disambiguation is performed: If the argument a matches both the parameter type f of p and g of q via a subtyping relation, the inheritance depth is taken into account: type A = object of RootObj B = object of A C = object of B proc p(obj: A) = echo "A" proc p(obj: B) = echo "B" var c = C()

    not ambiguous, calls 'B', not 'A' since B is a subtype of A

    but not vice versa:

    p(c) proc pp(obj: A, obj2: B) = echo "A B" proc pp(obj: B, obj2: A) = echo "B A" 120

    but this is ambiguous:

    pp(c, c) Likewise, for generic matches, the most specialized generic type (that still matches) is preferred: proc genT = echo "ref ref T" proc genT = echo "ref T" proc genT = echo "T" var ri: ref int gen(ri) # "ref T"

         Nim is based on overloading. Overloading is not just “syntactic
         sugar”, it is essential for static polymorphism.
         The follow example outlines how a family of procs called toJ
         can be used to load arbitrarily typed data:
          import std / json
          proc toJ[T: enum](e: T): JsonNode = newJInt(ord e) 1
          proc toJ(s: string): JsonNode {.inline.} = newJString(s) 2
          proc toJ(b: bool): JsonNode {.inline.} = newJBool(b)
          proc toJ(f: float): JsonNode {.inline.} = newJFloat(f)
          proc toJ(i: int): JsonNode {.inline.} = newJInt(i)
          proc toJ[T](s: seq[T]): JsonNode = 3
    

            result = newJArray()
            for x in s:
              result.add toJ(x) 4
          proc toJ[T: object](obj: T): JsonNode = 5
            result = newJObject()
            for f, v in fieldPairs(obj): 6
              result[f] = toJ(v) 7
          1 Enum values are mapped to JSON by using their ordinal
             values.
          2 string, bool, float and int are mapped to their JSON
             equivalents.
          3 seq[T] is mapped to a JSON array.
          4 Depending on the sequence element’s type call the correct
                                                                     121
              overloaded toJ proc.
          5 An object can also be loaded from JSON.
          6 Iterate over every field of the object via fieldPairs.
          7 Call the overloaded toJ proc for every field v of obj.
    

    19.1. Overloading based on 'var T' If the formal parameter f is of type var T in addition to the ordinary type checking, the argument is checked to be an l-value. var T matches better than just T then. proc sayHi(x: int): string =

    matches a non-var int

    result = $x proc sayHi(x: var int): string =

    matches a var int

    result = $(x + 10) proc sayHello(x: int) = var m = x # a mutable version of x echo sayHi(x) # matches the non-var version of sayHi echo sayHi(m) # matches the var version of sayHi sayHello(3) # 3

          # 13
    

    19.2. Lazy type resolution for untyped 

         An unresolved expression is an expression for which no symbol
         lookups and no type checking was performed.
    

    Since templates and macros participate in overloading resolution, it’s essential to have a way to pass unresolved expressions to a template or macro. This is what the meta-type untyped accomplishes: template rem(x: untyped) = discard rem unresolvedExpression(undeclaredIdentifier) A parameter of type untyped always matches any argument (as long as there is any argument passed to it). 122 But one has to watch out because other overloads might trigger the argument’s resolution: template rem(x: untyped) = discard proc remT = discard

    undeclared identifier: 'unresolvedExpression'

    rem unresolvedExpression(undeclaredIdentifier) untyped and varargs[untyped] are the only meta-type that are lazy in this sense, the other meta-types typed and typedesc are not lazy. 19.3. Varargs matching See Section 16.15, “Varargs”. 19.4. Overload disambiguation For routine calls “overload resolution” is performed. There is a weaker form of overload resolution called overload disambiguation that is performed when an overloaded symbol is used in a context where there is additional type information available. Let p be an overloaded symbol. These contexts are: • In a function call q(..., p, ...) when the corresponding formal parameter of q is a proc type. If q itself is overloaded then the cartesian product of every interpretation of q and p must be considered. • In an object constructor Obj(..., field: p, ...) when field is a proc type. Analogous rules exist for array/set/tuple constructors. • In a declaration like x: T = p when T is a proc type. As usual, ambiguous matches produce a compile-time error.