myst-lang / myst

A structured, dynamic, general-purpose language.
http://myst-lang.org
MIT License
119 stars 17 forks source link

Consider adding type contracting (protocols/interfaces/abstract types) #9

Open faultyserver opened 7 years ago

faultyserver commented 7 years ago

In dynamic, interpreted languages, the ability to define explicit contracts about the interfaces that values expose is often lost. Instead, these are traded for abstract definitions and/or implicit contracts that are often visually simpler, but less obvious when the contract is extended or re-implemented by another class/module.

The primary loss in these languages is the ability to enforce those contract requirements before they are encountered at runtime (if they ever are).

Another great side-effect of being able to enforce those contracts is predictable failure. That is, if a function requires that a contract be satisfied, then calls to that function with non-conforming arguments will fail before any of the function's body is executed. When the contract is not enforceable, it is common for functions to begin execution and fail midway through, resulting in a greater (often inferred, not necessary) dependence on exceptions and exception handling.

I think having Myst adopt a contracting system would be beneficial and worthwhile, and doesn't have to compromise readability or flexibility in the language. The roadmap for Myst already suggests explicit typing will be idiomatic in most cases, so the ability to specify more abstract behaviors follows suit to me.

Examples of languages that implement and enforce contracting systems:

Some considerations:

  1. What does the syntax look like? I'm somewhat partial to something like a use or implements directive at a module/class level for defining the contracts that the type should satisfy.
  2. How is this affected by re-openable classes? When is the contract enforced? After parsing finishes, or some time during interpretation (function call time?).
  3. Is anything more than module inheritance necessary? In most cases, contracts can be implicitly set up by defining and including modules with the contract definition into subtypes (see Ruby). This is effective, but still has most of the drawbacks of implicitness as mentioned earlier.
faultyserver commented 7 years ago

I spent a solid hour writing out an example usage of a ValueStore protocol as an abstract interface for storage types like YAML, JSON, databases, etc. What I got out of it is that there isn't much benefit to them beyond what including a module would be able to provide.

I like the distinction between "supplies this interface, but may fallback to a default implementation" that modules provide and "supplies this interface completely on it's own" from protocols. The latter seems useful, particularly for things like storage types, but I don't see a real use for them.

I'll leave this open in case a more convincing use case comes up.

faultyserver commented 7 years ago

I think I've found one legitimate use case for some kind of protocol enforcement, and it's actually very simple.

Take the following class:

class SomeCollection
  include Enumerable
end

This is semantically valid and not incorrect in any way. SomeCollection will now have access to all of the methods from Enumerable.

But, this obviously isn't actually correct, because there's no each method defined, meaning pretty much every method from Enumerable will end up raising an error.

This is something that could potentially be resolved by protocols before the code runs. This wouldn't be possible in one pass, as each could be defined lexicographically after the include. However, with a semantic/typing pass preceding interpretation (which I already think will be done to enforce function parameter structures), the set of methods defined on the class could be known before the include is evaluated, and a check for each could be performed with a protocol.

This would probably make use of an included hook:

module Enumerable
  protocol Each
    def each()
  end

  def included(base)
    unless base.implements?(Each)
      raise "#{base} included Enumerable, but did not define `#each`."
    end
  end
end

.implements? in the above is obviously a placeholder for some native-level check for meeting the protocol (otherwise it would be too slow to warrant something different than a simple responds_to? implementation). Note that whatever this check ends up being would be the same check used for protocol restrictions in function pattern matching described earlier.

With this, though, Myst could potentially tell you that your code won't work before any code is actually run! (Well, that would need a third pass for handling type finalization separate from evaluation, but it'd be possible! Natively!)

faultyserver commented 7 years ago

This was an interesting read from José Valim on how to properly implement protocols: http://blog.plataformatec.com.br/2014/06/comparing-protocols-and-extensions-in-swift-and-elixir/.