Copute - Advantages vs. Scala

  1. immutable
  2. mixin
  3. interface
  4. inferred
  5. method
  6. ADT
  7. function
  8. union
  9. cast
  10. match
  11. body
  12. static
  13. variance
  14. inherit
  15. partial
  16. line
  17. new
  18. operators
  19. bottom
  20. tuple
  21. implicit
  1. Untangled: Identifiers are always val, i.e. immutable. The assignment operator may only be used to initialize an indentifier. Thus, expressions are always referentially transparent.

    Copute does not have the dangling-else ambiguity, but this is not due to requiring a ‘block’ for each if. This ambiguity is eliminated by any language that requires every if to be paired with an else. Unpaired if only has a purpose (and thus is only allowed) in languages which allow mutable variables, e.g. Scala’s var. Thus Scala suffers the ambiguity. Copute has a returnif statement to avoid nested if-else for short-circuited function return.

  2. Untangled: Scala’s trait is replaced by interface and mixin. This unconflates interface and implementation. Interfaces may not contain the implementation of the method, i.e. the function body. Note, interfaces do contain the implementation of static methods.

  3. Untangled: Only interfaces may be accessed in expressions and referenced in type signatures. This unconflates interface and implementation, to facilitate uncoordinated extension via the principles of inversion-of-control, Hollywood, SPOT, and ‘D’ in SOLID.

  4. DRY: The implementation of a method must omit the type signature of its input parameter(s) and output, if already specified in an inherited interface. The type signature cannot be omitted if the method is overloaded, and the corresponding methods in the inherited interface(s) cannot be matched in sequential inheritance and intra-interface order. Scala infers the output type if possible from the function body, but it does not infer the type signature from inheritance.

  5. DRY: Interface members are always immutable references (to methods). Thus, there is no need prepend them with the val keyword. Scala also has the def keyword for methods and var keyword for member data (in Scala member data can be a function type).

  6. Unified: Every instance of data is an algebraic data type. Each ADT is a class name and any constructor parameter(s), and automatically creates an interface with a method for each parameter, i.e. an abstract data type. The parameter(s) are always immutable; thus, there is no need prepend them with the val keyword. Since there is no data declared, and thus only methods, in the body of a class, data is unified with interfaces.

  7. Lucid: Function identifiers have one consistent form, “identifier : type signature = anonymous function”. The assignment of the anonymous function (i.e. the implementation) is omitted for a method of an interface. As previously stated, the “: type signature” must be omitted for the implementation, where it is inferred from inheritance of an interface. Class constructors have the form “: type signature = anonymous function”, where the type signature does not include an output type (since it will always be the same as the type of the class).

    Scala has multiplicitous forms of function definition. Also, the type signature is interleaved with the function parameters, e.g. param1 : Type1, param2 : Type2. This is noisy. If it were employed in Copute, this interleaving would occur for the interface, but not for the implementation, of a method. This is because in Copute the type signature must not be repeated for the implementation of a method. Thus, the parameter list would appear differently in the interface and implementation.

  8. Unified: Copute has an unboxed logical disjunction (a/k/a union) type, e.g. Type1 | Type2. In Scala, it can emulated with boilerplate. Thus, in Copute there is no need for an Option[T] type, instead use T | None. The construction and destruction of a T | None disjunction eliminates the boilerplate for the boxing and unboxing, i.e. Some. There is a plan to make all disjunctions containing ... | None, automatically implement the Listable[... | None,...], Monad[... | None,...], Traversable[... | None,...] interfaces. Note there is a logical conjunction (a/k/a intersection) type for Copute, e.g. Type1 + Type2, and Scala, e.g. Type1 with Type2.

  9. Lucid: Since Scala’s, and thus Copute’s, type interface is localized, ambiquities can occur. To resolve cases of ambiguity when calling a type parametrized function, the type parameter(s) are not allowed to be explicitly declared, e.g. fold[List[Int]]( Nil,. Instead, casts must be employed, e.g. fold( Nil : List[Int],. This enforces a consistent resolution methodology. This methodology has the advantage that it explicitly declares which function parameter(s) caused the ambiguity.

  10. Lucid: Scala’s x match { case ... => ... case ... => ... } is replaced with x | ... -> ... | ... -> ... (and each body has an option form). When each vertical bar is horizontally aligned on a separate line, this formats to a vertical line of branches. This has the advantage of not repeating the conditional expression in an if-else construction if x == ... { ... } elseif x == ... { ... } or chained ternary operators x == ... ? ... : x == ... ? .... Note, the ternary operator is right-associative.

    An anonymous function that inputs only one parameter and contains a match on that parameter at the start of the function body, e.g. x -> x | ... -> ... | ... ->, x { x | ... -> ... | ... ->, or x { x | ... { ... } | ... ->, may be shorted to ... -> ... | ... -> or ... { ... } | ... -> respectively. This is possible because type signatures are not interleaved with parameters. Scala has a similar capability, which is documented in the subsection “Case sequences as partial functions” within the section “15.7 Patterns everywhere” of the book “Programming in Scala, First Edition, Version 6”.

    Note that the vertical bar, |, is not available as an operator. Any numeric types can define a replacement method and/or operator. The traditional binary or, and, and xor operators have a historic precedence error, thus user-defined operators and/or methods are an improvement.

  11. Unified: Each match case and anonymous function body must end with a right-hand-side expression, which may be optionally preceded by val and/or returnif statement(s). Each body can be in one of two forms, where the first form requires a semicolon, and the second requires a new line (and may not have a semicolon), after each statement, e.g. -> val ...; ... or

    { val ...
    ... }

    The second form is a ‘block’. The expressions in the first form may not contain a ‘block’. The first form must span only one source code line.

    The ending expression in the first form may not contain a non-parenthesized function expression nor match, except a non-parenthesized match in a function that inputs only one parameter. The parenthesized match is an unavoidable wart of the grammar would not exist if the shortened form was removed from the grammar. This is still an improvement over scala, which forces all match cases to be enclosed in braces.

    The rhs expression in a val statement of a ‘block’, must be parenthesized if it is a function expression and the type signature of the function is omitted. The only other possible way to restructure the grammar, would be to force all the function parameters everywhere to be parenthesized (as Scala does).

    The if-else construction must only employ block bodies, e.g. if ... { ... } else { ... }, because the match and ternary construction provide for the majority of cases where a terse construction is needed. Also, this eliminates the requirement to parenthesize the conditional expression which follows if, which would otherwise be necessary in order to provide a visual boundary and alleviate grammatical boundary ambiguities.

    Scala has a more verbose non-block form of if-else instead of a ternary operator. In both Copute and Scala, the single colon : is not an identifier because it is used in casts. This is why Copute’s ternary operator must require a nested ternary, between the ? and the :, to be parenthesized.

  12. DRY: This is Copute’s notion of Haskell’s typeclass. An interface method prepended with the static keyword, can be implemented in the body of that interface, e.g. interface A { static id : type signature = ...}, or any interface that inherits from it, e.g.

    interface A { static id : type signature }
    interface B >- A { A.id = ... }

    Same as for non-static methods, all static methods of a class must have or inherit an implementation. These static functions do not have an implicit this parameter. Thus, they can be called statically on an interface identifier, or a type parameter, that resolves to a (possibly inherited) implementation. Scala can emulate this (see “Now What?” section) but with significant boilerplate that violates SPOT.

  13. Lucid: The default with no variance annotation means covariant; whereas, in Scala it means invariant. Covariance is more likely because all interfaces are immutable in Copute, and many interfaces will use the type parameter in only a covariant position, e.g. a method output type. The annotations <-> and -> mean invariant and contravariant respectively. These visually imply respectively whether the type parameter can be used for both method inputs and output, or just inputs.

    Scala’s annotations + and - mean covariant and contravariant respectively, but the symbols don't visually imply a correlation to method input and output types.

    For both Copute and Scala, the rules for where covariant and contravariant types can be used are slightly more complex than “method input and output types”.

  14. Lucid: Upper and lower bounds follow the type operators >- and -< respectively. These operators appear on the right side the type parameter to which they apply. These visually convey which side of the operator is the parent supertype that forks to the child subtype on the other side. A relationship to inheritance is implied, because the >- symbol is also used to indicate inheritance when defining interfaces, classes, and mixins.

    Scala uses <: and >: respectively for the upper and lower bounds. The colon in these operators visually implies a relationship between type bounds and type signature, since the operator for type signature is a color :. Scala’s choice does not convey any relationship between bounds and inheritance, since it has the keywords extends and with to indicate inheritance when defining traits, classes, and mixins.

  15. Lucid: Partial type application uses the underscore _.

    interface Monad[ Sub[T] : Monad[Sub,T], T ]
    class State[S,T] >- Monad[State[S,_],T]

    State[S,_] is a partially applied type, because Monad expects Sub[T], not Sub[S,T]. Scala’s partial type application syntax is more verbose, which is explained here. See also section 5 of Generics of a Higher Kind.

  16. Lucid: A semicolon ; is not allowed to appear at the start nor end of a source code line (‘start’ and ‘end’ are calculated as if indenting and comments were removed). Semicolons may only and must appear after statements in the non-block form of the body of an anonymous function or match. Scala allows an optional semicolon any where an expression could be terminated, including at the start or end of the source code line.

    In both Copute and Scala, statements and expressions can span lines. Where a semicolon doesn't separate expressions, Copute and Scala have different rules to remove the two potential ambiguities that determine the boundary between expressions.

    1. A left parenthesis, (, could potentially represent a function call operator on the left-hand-side expression, or a grouping or tuple that begins the right-hand-side expression.

      Copute and Scala both require the function call operator to be on the same line as the lhs expression. When the left parenthesis is at the start of a line, it begins a rhs expression.

    2. An operator, e.g. -, could potentially represent a binary (i.e. infix) operator on the left-hand-side expression, or a unary operator that begins the right-hand-side expression.

      Copute requires prefix unary operators to be preceded by the start of input, an empty line, another operator, or one of the following symbols, (, {, ->, |, or =. Thus to force the start of a rhs expression, insert a left parenthesis before the unary operator if it would otherwise be a binary operator for a lhs expression. Scala interprets a potential prefix unary operator as a binary or unary operator, depending on if it appears at the end or start of the line respectively. See section “4.2 Semicolon inference” of the book “Programming in Scala, First Edition, Version 6”. Since the use of prefix unary operators to begin expressions is less frequent than the placement of a binary operator at the start of a line, Copute’s grammar will reduce programmer error due to this ambiguity.

  17. DRY: There is no new keyword. Call the constructor to create an instance of a class. Scala requires prefixing the constructor call with new, when instantiating a class that was not prefixed with case.

  18. Lucid: Operators.

    PrecedenceOperatorsArityFixityAssociativityDescription
    1.binaryinfixleftmember
    2()unarypostfixleftcall
    3other non-letters1unaryprefixrightuser-defined
    4other non-letters1binaryinfixright if ends : or >2user-defined
    5* / %binaryinfixleftmultiply, divide, modulo3
    6+ -binaryinfixleftadd, substract3
    7< <= > >=binaryinfixleftrelational4
    8== !=binaryinfixleftequality5
    9&&binaryinfixleftlogical-and
    10||binaryinfixleftlogical-or
    11?:ternaryinfixrightif-else
    :unary6postfixleftcast
    12=binaryinfixnone7assignment

    1Prefix unary are distinguished from binary infix operators by context.

    2Both Copute and Scala offer the right-associative option, for the infix binary operator that ends with :, which must be a method of the right operand. Also, Copute offers the right-associative option, for the infix binary operator that ends with >, which must be a method of the left operand. Copute offers the left-associative option, for the infix binary operator that starts with <, which must be a method of the right operand. All other user-defined infix binary operators are left-associative and must be a method of the left operand. This flexibility is required for intuitive use of operators, e.g. see Applicative.

    3User-defined with these suggested meanings. Must output the same type as one of the operands, to reduce confusion as to what overloaded operators do.

    4User-defined with suggested relational meaning. Must output the same type as the equality operator.

    5Operator is not user-defined, but the Any.equals is called by the operator, and this method may be overridden. For an AnyRef type, the operator might first check for unequal references before calling Any.equals.

    6Colon’s right operand is a type, not an expression. So the colon is postfix unary in terms of the expression, and infix binary in terms of the type.

    7Chained assignments, e.g. val x = y = 1 are not allowed, because there is no practical utility of more then one identifier for the same immutable value.

    Scala’s chart is as follows.

    PrecedenceOperatorsArityFixityAssociativityDescription
    1.binaryinfixleftmember
    2()unarypostfixleftcall8
    3+ - ! ~unaryprefixrightuser-defined
    1st char not on on this chart1unarypostfixleftuser-defined
    4non-letters, 1st char not on on this chart1binarypostfixright if ends :2user-defined
    1st char is
    5* / %binaryinfixright if ends :2user-defined
    6+ -binaryinfixright if ends :2user-defined
    7:binaryinfixright if ends :2user-defined
    8< >9binaryinfixright if ends :2user-defined
    9= !9binaryinfixright if ends :2user-defined
    10&binaryinfixright if ends :2user-defined
    11^binaryinfixright if ends :2user-defined
    12|binaryinfixright if ends :2user-defined
    13any letterbinaryinfixright if ends :2user-defined
    14=binaryinfixrightassignment

    8Function arguments may optionally be supplied without enclosing them in parenthesis. Thus, when the identifiers are letters, there are scenarios of no visual distinction between operands, arguments, postfix unary, and infix binary operators. Also, optional parenthesis for function calls means there is no visual distinction between a call with multiple arguments and a call with a tuple argument, since tuples can be abbreviated with parenthesis. This contributes to making some Scala code very difficult to read.

    9Apparently the specification and section “5.8 Operator precedence and associativity” of the book “Programming in Scala, First Edition, Version 6” have the precedence erroneously transposed, and the chart above is correct.

  19. Lucid: The bottom type is All, which is a more intuitive name than Scala’s Nothing.

  20. Lucid: The type of a tuple, e.g. Tuple2[Int,Int], may be abbreviated [Int,Int].

  21. Lucid: There are no implicits, which are a major reason Scala code can be difficult to comprehend. The Copute compiler does use Scala's implicits to implement some of Copute’s features. These unifying features eliminate boilerplate and simplify comprehension, where otherwise implicits would be needed, e.g. static (typeclass) and union (disjunction) type.

DRY’ means Don’t Repeat Yourself.