auto addone = [](auto x) { return x+1; };
That monstrosity at the top of the article is needlessly complex because C++11 didn't support generic lamdbas. auto addone = [](auto x) { return x+1; };
That monstrosity at the top of the article is needlessly complex because C++11 didn't support generic lamdbas. addone1(x) = x+1
Completely generic with type inference, and compiles to optimal machine code. But it also suffers from the "latent type errors deferred to the user" that the article discusses. Calling it with a type that does not support adding 1 will raise an error for a non-existing "+" method: addone1("0")
ERROR: MethodError: no method matching +(::String, ::Int64)
This is pretty understandable in this case, but we may prefer annotating the addone function so that it can only be called with types that support adding 1. Using the (very conservative) notion that only numbers can have 1 added to them, we could use Julia's type hierarchy to restrict the types for which addone may be called: addone2(x::T) where T<:Number = x+1
Calling it with an incompatible type now properly points to the outer level: addone2("0")
ERROR: MethodError: no method matching addone2(::String)
But this type of dispatch restrictions rely on a type hierarchy, causing similar problems as class hierarchies in OO languages. For example, the user might have defined their own type that supports addition but not multiplication. Using addone on this type makes sense, but the type shouldn't be a subtype of Number. Such more flexible dispatch scenarios can be achieved with traits. Although Julia currently doesn't have direct language support for traits, they can be implemented inside the language, with macros for syntactic sugar. With traits, the previous example becomes: using SimpleTraits
@traitdef CanAddOne{T}
@traitimpl CanAddOne{T} <- issubtype(T,Number)
@traitfn addone3{T; CanAddOne{T}}(x::T) = x+1
Still relatively straight-forward to write and read, and the trait-restricted addone has the same performance as the original one. For incompatible types, the error message still points to the outer level: addone3("0")
ERROR: MethodError: no method matching addone3(::Type{SimpleTraits.Not{CanAddOne{String}}}, ::String)
Importantly, the user can extend the CanAddOne trait (with additional @traitimpl lines) to cover their own type, without being forced to make it a subtype of Number. add1 = (+1)
> add1 23
24
> add1 2.01
3.01
> add1 "0"
No instance found...
But that error is at compile time.My Haskell is very rusty, but IIRC it will deduce that the argument to add1 must be Num, because of the type constraint on (+):
(+) :: Num a => a -> a -> a
So if I'm not mistaken, your add1 is equivalent to my addone2, i.e. incorporating a type constraint based on the type hierarchy. I'm curious: what's the equivalent of my addone3 in Haskell? How would you define add1 so that it also works on a type T later defined by the user, with T just supporting addition but T not subtype of Num? Do you need to define your own addition function (acting as a proxy for (+) for Num types)?Haskell also provides a ton of options for generic programming that provide similar power (still statically typed). GADTs (Generalized Algebraic Data Types) might be of use here. There are also existential types and type classes.
What you probably really want though is a dependently typed language like Idris or Agda. If you're not familiar, here's the one liner: Java generics are types that depend on types (like a List of Bools). Dependent types are types that can depend on values (like a List of exactly 3 Bools). So in theory you should be able to define the type you're looking for here, but I don't know what the concrete syntax would be.
The problem with dependently typed languages right now is that the surface languages and concepts tend to be hard for people to grok. Personally I think meta-programming facilities and DSLs will help with this. Ther are lots of languages heading in that direction.
Working code poured out almost as fast as I can type.
If I had to think about some propeller-head academic dependent type nonsense, I'd still be writing it come March 2018.
> The problem with dependently typed languages right now is that the surface languages and concepts tend to be hard for people to grok.
Anything hard to grok is a regression in tooling.
Why would I want to grok something difficult, when I'm finding programming easy and don't have any issues with getting the desired behavior out of the machine.
All I want to be grokking is how the bits of the run-time representation in the machine are coming together to solve whatever is being solved.
If you're programming a single computer the existing languages and tooling are good. What I'm interested in is distributed compute and compute over heterogenous architectures (CPUs, GPUs, FPGAs, microcontrollers, etc).
For simplicity, let's limit ourselves to the simple case of HTTP-based service-oriented systems (or microservices). The state of the art has us spending a lot of time considering serialization, protocols, communication failures, managing identities, making authorization decisions, routing, etc. Things that should be completely orthogonal to the problem we're trying to solve end up being tightly coupled with our business logic. This becomes exponentially harder to manage as you add languages to your stack.
There is a lot of research around solving these problems. Unfortunately, all of it starts with a formal system that is "declarative" (or at least "algebraic") and doesn't mesh well with existing popular languages and tooling. Personally, I think we need to come up with a solution that lets us keep the existing tools that are good for programming a single component, and use some of this newer technology to build a smarter platform / underlay. The bridge between the existing languages and this underlay would be DSLs. Or, more precisely, "abstract languages" that can be implemented as DSLs in a variety of programming languages and may or may not have their own concrete syntax (think SQL+ORMs).
Sort of vague, I know, but hopefully that kind of makes sense?
add1 =: >:
add1 1r3
4r3
add1 0 1