
Notes on Idris (2017) - jxub
https://thebreakfastpost.com/2017/12/02/notes-on-idris/
======
ainar-g
Recently I talked with my cousin, who is also a software engineer, about
dependent types, contract programming, invariants, and all that. Among other
things I've told him about "type-safe printf" that you can write in Idris. He
then asked me, why are there only purely functional languages with this
feature. I had no actual answer.

So, does anybody know, why there still isn't a "C/Go/Java with dependent
types"? There is support for runtime checking via invariants in e.g. D, but no
pre-compile-time checker.

Another question, is there (probably commercial) software that allows you to
augment your code with invariant annotations and then check them as a pre-
compilation step?

~~~
saurik
Technically (and truly), C++ has dependent types, as types can depend on
values [edit: matt-noonan has corrected me and pointed out this is wrong, but
I think most of this comment is still useful]. (I can't say much about D, but
I would believe it does as well; I could also see them having "simplified"
away a really critical piece, though.)

A prototypical concept you would expect to find in a dependently typed
language is "the ability to have a function that concatenates to arrays of
sizes X and Y and returns an array of size X+Y that can be safely verified
against further functions that also require arrays given as arguments to have
specific lengths".

C++ is a bit indirect in its semantics here, but you will note that most other
languages with generic types cannot express this thought (using either real
integers or simulating them with types, either of which can be done in C++).

One could easily imagine "filling out" C++ to allow compile time type argument
manipulation of more value types without even stressing its model, and in fact
that is exactly the direction the language has been going in with constexpr.
Here is an article on how to build a compile-time printf using C++14.

[http://blogs.microsoft.co.il/sasha/2015/01/29/compile-
time-r...](http://blogs.microsoft.co.il/sasha/2015/01/29/compile-time-runtime-
safe-replacement-printf/)

The only real limitation in C++ is that these values have to be relatively
"concrete": at some level there has to be a _number_ that can be calculated,
as you can only deal with values as you don't have the amazing ability to do
unification of program fragments like you have access to in Idris (like, you'd
like to be able to say "the length of this array is the same as the length of
this runtime string", and then after a bunch of array manipulation claim that
you return a string five characters longer than the input string). Again,
though: this could be added to C++ without breaking its underlying model. And
it definitely is not required by the concept of "dependent types" [edit: matt-
noonan points out it is, in fact, required for a language to have "dependent
types" :(.]

~~~
matt-noonan
You can make C++ types that depend on constant values computed at compile-
time, but this isn't the same as dependent types. In a hypothetical
dependently-typed C++, I would be able to do this:

    
    
        template <typename T, int N> struct vec { T data[N]; };
        vec<T,n> repeat(int n, T const& x) {...}
    

Note the run-time value n appearing in the return type of `repeat`. Because of
this, repeat (if we could write it) has a dependent product type: the actual
return type varies based on an argument's value.

~~~
saurik
Is this really required by the definition of dependent types? I definitely
agree that this is a difference with Idris (which can parameterize types over
not just values but essentially program fragments due to its proof checker and
unification semantics), but I was under the impression that this was not
required to be considered dependent types.

~~~
matt-noonan
Yes, you really need at least Pi types (dependent products, as above) or Sigma
types (dependent pairs) to have dependent types. So you _could_ get away with
it in Python by munging a return type based on a parameter, but not C++ or
most other statically typed languages!

Unfortunately it seems like there is a lot of fog around this. Even the
wikipedia page for dependent types botches the first example: "pairs of
numbers where the second element is greater than the first" is just a normal
type. A dependent type would be something like "pair<int,vec<T,n>>" where the
n is supposed to equal the first element of the pair.

The waters are muddied even further by the fact that you can reproduce many of
the canonical examples for dependent types using other mechanisms, as you
pointed out below for the vector-append operation!

Edit: After complaining a bit about the Wikipedia page, it was pointed out
that you _can_ think of that example as a Sigma type. I contend it is not
obvious unless you have already spent time thinking about dependent types,
though.

------
icebraining
_a simple program took 25 seconds or more to build_

This is surprising, I haven't had that experience, although I've been using
the JavaScript backend instead, maybe it's much faster? It takes a few
seconds, but not that much, and I'm on mere X220.

Like the author, I also struggle to write clean code in it. I've been writing
a personal project to learn functional programming, Idris, Lambda and
DynamoDB, and it's been going slowly. Reading the book, and embracing the
suggested development method (type, define, refine) has been helpful, and I'm
enjoying the whole process, despite a couple of snags with the JS interface.

~~~
iainmerrick
I was nodding along until I hit that point too. 25 seconds for a simple
program??!? That would be a complete dealbreaker.

~~~
fiddlerwoaroof
It’s not as bad as it sounds because you don’t have to compile your program
while you write it. Rather, you send the program to a repl and the type
checker tells you about problems. You can even run the total parts pretty
quickly in the repl.

The only issue is that the language server seems to have a memory leak or
something that leads to it taking a huge amount of memory and getting sluggish
over the course of an extended development session.

~~~
gameswithgo
>The only issue is that the language server seems to have a memory leak or
something that leads to it taking a huge amount of memory and getting sluggish
over the course of an extended development session.

that is not good advertising!

~~~
fiddlerwoaroof
I'm sort of hoping someone here will tell me that I'm doing it wrong. Because,
this has always been the showstopper for me.

~~~
iainmerrick
The language sounds very interesting otherwise, so I’d like to hear of any
fixes or workarounds too.

~~~
edwinb
There are various reasons why Idris is slow, but it generally comes down to it
being because the current system is the result of lots of experimentation
about how to even implement a dependently typed language in the first place,
and what it should look like. It needs quite a bit of re-engineering - I
prioritised ease of playing with features over efficiency.

The good news is that I'm working on a new version, taking into account the
many things I know now that I didn't when starting on version 1. Don't hold
your breath though, it might take a while...

In the end, it's a research project, and our job isn't so much to make a good
product as to get people excited about the ideas. Mostly we do this by trying
to be obviously having lots of fun. But I'd still like a better
implementation, because then we can have even more fun.

------
alexashka
I've also bought and worked through the book. It's excellent.

My conclusion was that Idris is an academic language (lack of libraries),
followed by realizing that the parts of Idris that I like, are almost all
covered by Haskell extensions.

For me, Idris is a great peek into what Haskell should/could be, if extensions
were better documented and the synergy between them, explained to mere
mortals.

~~~
KirinDave
I've got a few network services working with Idris and they run okay. The
compilation time grows so quickly you can't use it for very large projects
though.

Honestly, Haskell dependent-typing is SO MUCH MORE COMPLEX than Idris's "well
what if I just write a function that takes a type?" approach, it's not even
funny. Even the author of singleton says it's a rough landscape.

Haskell's going to get much closer with QuantifiedConstraints in ghc 8.6, in
that it'll simplify a lot of this work, but it's still years out from the
simplicity and library unification.

~~~
matt-noonan
Can you elaborate on the DT / QuantifiedConstraints connection a bit? I
haven't thought enough yet about QuantifiedConstraints to see the everyday
benefits, but I'm getting contact-excited by everybody else's anticipation of
it. I'd love to get some concrete benefits to hold in mind.

------
kqr
Slightly unrelated but if all you do is stick around in do notation, you will
not see why Haskell is the greatest imperative language. That starts happening
once you break out of the traditional notation.

~~~
cannam
Can you expand on that?

~~~
kqr
I wish I could. Honestly! It's just that while I have experienced it
firsthand, I have not spent enough time thinking about it and tinkering with
it to be able to succinctly and accurately put into words what it is about.

Some loose thoughts that may or may not make sense:

\- In Haskell, effectful computations (such as prints, variable updates, and
so on) are first-class values (like integers, strings, and so on). This is the
big idea.

\- When effectful computations are first-class values, you can store them in
data structures like lists. You can associate them with keys in maps. You can
even stick them in priority queues.

\- When effectful computations are first-class values, you can pass them
around as arguments, or send them off as return values.

\- When effectful computations are first-class values, you can transform their
results, you can chain them together and you can create branches in them.

\- When effectful computations are first-class values, you can perform them,
then perform then again, then perform them 12 times, then discard them. Or
discard them first and never perform them.

\- When effectful computations are first-class values, you have decoupled
"describing the action" from "executing the action". You can handle the
effectful computation without being afraid of performing the effect.

\- When description is decoupled from execution, you can abstract over the
flow of control. You can have a loop that doesn't always increment its
counter, because the incrementing operation is specified in a list somewhere
instead.

\- Abstracting over the flow of control can be about as confusing as it is
dynamic and powerful. It can also be the most clear and obvious solution to
some problems.

\- Effectful computations as first-class values is a little like functions as
first-class values: when you have tried it, you've found it's so convenient
that you'd like to have it everywhere. (It's also a little like first-class
functions in the sense that you can emulate first-class effects with first-
class functions, but it easily becomes unwieldy.)

\- Effectful computations as first-class values is even a little like Lisp
metaprogramming. In Lisp, the flow of control can be rewired dynamically,
because the code itself is a Lisp data structure. In Haskell, the flow of
control can be rewired dynamically, because statements don't have to be
executed the second you see them.

\- The do notation, when used the traditional way, pulls a curtain over how
effectful computations are first-class values. Computations in do notation are
(to a first approximation) executed as soon as they are seen, because do
notation was designed to intentionally hide the ways in which Haskell is a
more powerful imperative language.

Sorry if that made things even more confusing than it cleared up.

\----

Here's the most basic concrete example I can think of:

    
    
        repeated = do {
            putStrLn "Hello";
            putStrLn "Hello";
        }
    

_Huh, the same action is performed two times. I guess I should put it in a
variable._

    
    
        somewhatDRY = do {
            let greet = putStrLn "Hello";  -- not executed when put into variable
            greet;                         -- executed once here
            greet;                         -- and again here
        }
    

_Oh, the user wanted to specify how many greetings to print._

    
    
        countGreetings number = do {
            let greetings =                     -- infinite list of prints,
                    repeat (putStrLn "Hello");  -- none of which is executed
            let enough =                        -- we take the first few prints,
                    take number greetings;      -- still without executing any
            sequenceA_ enough;                  -- now we execute just the first ones
        }
    

_Huh, I guess I don 't even need the do notation anymore._

    
    
        countGreetings number =
            let
                greetings = repeat (putStrLn "Hello")
                enough = take number greetings
            in
                sequenceA_ enough
    

\----

In these examples I used sequenceA_, which is a plain library function that
takes a list of effectful computations and returns a single effectful
computation. The effect of the returned computation is the same as if all the
effectful computations in the list was called one after the other.

I mention it's a plain library function because you wouldn't be able to define
such a function – not even in the standard library – if effectful computations
weren't first-class values...

\----

I wrote a comment in the example saying "now we execute just the first ones".
That's a lie, actually. Because like any effectful computation, also the
return value of countGreetings is a first-class value. Meaning we could assign
the return value of countGreetings to a variable, or stick it in a list, and
so on, and nothing would be executed.

In order to avoid uncomfortable déjà vu, I'll In order to avoid uncomfortable
déjà vu, I'll just leave off here and conclude that when effectful
computations are first-class values, there is, in a sense, two phases when the
code is run. First there's an evaluation phase, where effectful computations
are constructed and passed around. Then there's an execution phase, which
actually executes these effects. If your effectful computation isn't hit by
the execution phase, it will never be executed – no matter how many times the
evaluation phase goes over it.

~~~
cannam
Thank you! This is a really nice comment that gives plenty to think about.
It's delightful that, two days later, you actually have expanded in compelling
detail on what initially looked to be something of a throwaway remark.

I think that Idris tries to make the evaluation/execution distinction a bit
more apparent. In particular, when you enter something effectful at the REPL,
only the evaluation happens automatically and the result is printed in an
internal form. This is confusing at first, but you can see that there's
something tantalisingly interesting going on.

~~~
kqr
No problem whatsoever! I only wish I could have done it better.

I do like the way the Idris REPL does that, from your description. Didn't know
that. How do you run it for effects?

~~~
cannam
Using the :exec REPL command.

    
    
      Idris> putStrLn "Hello, world!"
      io_bind (prim_write "Hello, world!\n")
              (\__bindx => io_pure ()) : IO ()
      Idris> :exec putStrLn "Hello, world!"
      Hello, world!

