
Dependent Haskell Is the Future (2018) - allenleein
https://serokell.io/blog/why-dependent-haskell
======
pron
The problem is that dependent types (and the very idea of "correctness by
construction") suffer from fundamental, serious limitations -- both in theory
and in practice -- that the author does not seem to be aware of. He also does
not seem to be aware of all the _other_ approaches for software verification
and correctness (sound static analysis, model checking, concolic testing) that
have so far shown greater promise, and formal methods research focuses on them
rather than on dependent types (the approach that scales least and suffers
from the most limitations); in fact, last I spoke with a dependent type
researchers, he told me that now they're trying to _weaken_ the soundness of
their type systems (sort of gradual-dependent-types) to try and fight the
problems types suffer from. I'm not saying dependent types don't have some
uses, or that they won't change enough so they could play a central role in
software correctness, but thinking that they're the future of software
correctness (in their current form) shows a complete lack of familiarity with
this field.

~~~
ernst_klim
> The problem is that dependent types (and the very idea of "correctness by
> construction") suffer from fundamental, serious limitations

It would be great if you've mentioned them and briefly described why are they
limiting.

> model checking

Is extremely complex. Proving complex programs using constrain solving would
be too expensive, if possible. I would argue that proving stuff manually using
SMT for trivial cases, as F* does, would be much more productive than doing
the whole proof using any sort of constrain solver (if this would ever work
for a sufficiently big program).

The main problem of a constrain solvers is that they don't scale well. When
you proved lemmas for a small block of code, you simply reuse those for higher
order proofs. Constraints are being accumulated (tho not linearly) when the
program grow and so does its state.

~~~
pron
> It would be great if you've mentioned them and briefly described why are
> they limiting.

I've done so here:
[https://news.ycombinator.com/item?id=20714845](https://news.ycombinator.com/item?id=20714845)

> Is extremely complex. Proving complex programs using constrain solving would
> be too expensive, if possible.

The same is true for deductive proofs, of which dependent types are a
particular instance, only the problem is far worse. It is true that there are
cases where deductive proofs are more feasible than model checking, but the
converse is true in a much wider portion of cases. That's why deductive proofs
are rarely the first choice, and they're used selectively, usually only after
other methods have failed. Overall, deductive proof, although it certainly has
its place in certain circumstances, is the least scalable verification method
we have. This means that it's the last method you want to use by default, let
alone bake it into your type system.

> Constraints are being accumulated (tho not linearly) when the program grow
> and so does its state.

This is true regardless of the verification method used. See my blog post
here: [https://pron.github.io/posts/correctness-and-
complexity](https://pron.github.io/posts/correctness-and-complexity)

> When you proved lemmas for a small block of code, you simply reuse those for
> higher order proofs.

No, you can't "simply" reuse results. Here's an example (given in the post in
Java, my preferred language, but as this is a post about Haskell, let me show
it in Haskell):

    
    
        foo :: (Integer -> Bool) -> Integer -> Integer
        foo p x
            | x <= 2 || odd x = 0
            | otherwise = head [i | i <- [x, x-1 .. 1], (p i) && p (x - i)]
    
        bar :: Integer -> Bool
        bar x = null [1 | i <- [x-1, x-2 .. 2], s <- [x, x-i .. 0], s == 0]
    

These two subroutines are very simple, clearly terminating, and you can easily
prove almost any property of interest about each of them in isolation. But now
I claim that their composition `foo bar` never crashes (on `head`); can you
prove me right or wrong? Feel free to "simply" reuse any proof about any of
them.

The idea that properties compose and that you can cheaply reuse knowledge
about components to prove stuff about their composition is just mathematically
false. There are a couple of results I mention in my blog posts that show
that. We know that if you have a composition of components X_1 ∘ ... ∘ X_n
then the cost of verifying some property of their composition is not only not
polynomial in n, it's not even exponential in n (it's not any computable
function in n, I think). I.e. it is a mathematical result that you _cannot_ ,
generally, hide the complexity of components to make verification of their
compsition scale with their number.

~~~
ernst_klim
> Dependent types rely on deductive proofs for verification, and deductive
> proofs (the "proof theory" part of a logic) are the least scalable
> verification method as they are least amenable to automation.

What? You can generate proofs for the trivial cases using various algorithms
like constraint solving.

Things like sledgehummer tactic, omega tactic, the whole F* language simply
prove you wrong (by construction ;)

You need to prove manually only complex cases, where your constrain solver
would not manage to generate the proof.

> But now I claim that their composition `foo bar` never crashes (on `head`)

If you proved that foo does not crashes on head for any predicate P and any
integer X, and bar terminates, than foo bar terminates. What's the problem?

> The idea that properties compose and that you can cheaply reuse knowledge
> about components to prove stuff about their composition is just
> mathematically false. There are a couple of results I mention in my blog
> posts that show that.

Could you refer to the proof of that, as well as give the definition of
``cheaply'' and especially the evidence that a composition of lemmas and
theorems would be more expensive than a constraint solvers-based methods?

I didn't state that proving is easy or cheap, just that manual proving with a
usage of constraint solvers for trivial cases is more scalable and makes more
sense for sufficiently big programs.

~~~
pron
> What? You can generate proofs for the trivial cases using various algorithms
> like constraint solving. Things like sledgehummer tactic, omega tactic, the
> whole F* language simply prove you wrong

I don't understand this simple proof. How does allowing some automation prove
that it's not the method least amenable to automation?

> If you proved that foo does not crashes on head for any predicate P and any
> integer X, and bar terminates, than foo bar terminates. What's the problem?

Because that would make the verification problem even harder (possibly to the
point of undecidability). And this approach simply doesn't make sense.
Clearly, the important correctness properties of your program arise from the
composition of all components of your program (or you wouldn't need them). If
the same correctness property arises from each component in isolation, then
you wouldn't need all others. Most correctness properties are just not
compositional (inductive), and the belief that a non-compositional property
can be proven by combining results about the components at a cost that scales
_with their number_ (polynomially, exponentially -- take your pick) is
mathematically false.

> Could you refer to the proof of that, as well as give the definition of
> ``cheaply'' and especially the evidence that a composition of lemmas and
> theorems would be more expensive than a constraint solvers-based methods?

I never mentioned "constraint solvers-based methods" (and I'm not even sure
what you're referring to [1]). Once you need to prove something, the method is
irrelevant, and the theoretical limitations kick in. Showing that one method
is easier or harder for instances encountered in practice requires empirical
research, so I can't say whether their harder or easier, just that so far
deductive proofs are the least scalable in practice. Finally, I _can_ say that
we cannot in general decompose correctness to make it easier. Some relevant
results are in the papers mentioned in my post in the sections "Correctness
does not decompose" and "Language abstractions cannot generally be exploited".

[1]: Perhaps you're referring to SMT solvers and the like, but they are rarely
used in isolation. They are used as components in deductive proofs, model
checkers and concolic tests. As usual, they are more effective when used in
the latter two than in the first.

~~~
ernst_klim
> Because that would make the verification problem even harder (possibly to
> the point of undecidability). And this approach simply doesn't make sense.

What? If foo terminates and bar terminates, than foo bar clearly terminates.
How does this make no sense?

> Most correctness properties are just not compositional (inductive), and the
> belief that a non-compositional property can be proven by combining results
> about the components at a cost that scales with their number (polynomially,
> exponentially -- take your pick) is mathematically false.

Ok, let's check your blog and what paper you mention

> A Parametric Analysis of the State-Explosion Problem in Model Checking⋆

What? You state that proving doesn't scale referring to constraint based
solutions? Sure, they don't scale.

What this foobar example has to do with this tho? Foo does not terminate due
to exception, and so does Foo Bar. How doesn't this compose?

> I don't understand this simple proof. How does allowing some automation
> prove that it's not the method least amenable to automation?

Because you can automate anything you can prove with a SMT or model checker?

~~~
pron
> How does this make no sense?

Because most correctness properties aren't compositional.

> What? You state that proving doesn't scale referring to constraint based
> solutions? Sure, they don't scale.

No, you've misunderstood the paper, and missed the part of the post where I
provide necessary context. Like a lot of work in computational complexity
theory, they're referring to the model checking _problem_ , i.e. the problem
of determining whether a program satisfies some property, not to any
particular verification algorithm. Their results apply to all methods (their
proofs don't not rely on any particular verification method but show hardness
results).

Also, it's funny that you say "they don't scale." You're right, none of the
formal methods we have scales nearly as well as we'd like, to the point we can
say "they don't scale" [1]. Of them, the one that scales _the least_ is
deductive proofs. That's the main reason it's the formal methods that is used
the least.

> constraint based solutions

I don't know what you mean by that (SMT?), and I don't recall mentioning
those.

> Because you can automate anything you can prove with a SMT or model checker?

Yes, model checkers are completely automatic.

SMT is a class of algorithms and techniques used in various verification
methods -- deductive proofs, model checking, static analysis, concolic testing
-- not a verification method in itself, so I'm not sure what you mean exactly.
Sometimes they're used to prove some logical proposition directly, but they're
not used as a complete software verification tool, because they're rather
limited on their own.

[1]: When applied to the code directly. We have ways of scaling formal methods
by verifying high level specifications.

------
kasperni
> Why Dependent Haskell Is the Future of Software Development

I find this hard to believe, considering probably less then 1-2% of developers
in the world have any clue to what this article is about.

~~~
onion2k
This is probably because it's about cutting edge language design. The feature
the article is about (dependent types) might well be the future of software
development and could end up in every strongly typed language, but because
it's new and Haskell's functional syntax looks weird compared to C-based
languages, it makes the headline sound a bit hyperbolic.

~~~
wbhart
I wouldn't disagree that dependent types are great for certain specialised
problem domains, but they've already been around for ages and didn't yet
revolutionise the world of software design. I think the sentiment that this is
unlikely to make a big splash is totally on point. Languages have to balance
ease of use with features in order to become very popular. Nothing as purebred
as Haskell is likely to become the next Python or Visual Basic. Real work
horses are dirty, messy machines. Of course this new feature might turn out to
be popular for Haskell users.

~~~
antisemiotic
>but they've already been around for ages and didn't yet revolutionise the
world of software design.

I think it's mostly because languages with dependent types are awfully clunky
to use. Perhaps dependent typing is even a dead end, but I wouldn't count out
other ideas such as refinement typing just yet. I think it will take a lot of
work and failed attempts (such as typestate in early Rust) to get fancy type
theory into mainstream languages (just like it took a while for Java to get
lambdas).

Case in point: static analysis tools for C++ are getting more capable and
popular recently.

~~~
krapht
Formal methods in general are extremely clunky to use. Just see how much hate
Rust gets for making it extremely difficult to make an intrusive linked list.
And memory safety is only one kind of correctness assertion.

So I'm gonna be a pessimist here and say: nope, not gonna be the future,
because programmers are lazy and "ship it now" is better than "prove the bugs
don't exist".

~~~
yakshaving_jgt
Not all programmers build WordPress plugins.

Some programmers write software for aircraft. The "ship it now" maxim doesn't
exist in that context.

~~~
lonelappde
No one is writing aircraft software in Haskell. They need good control over
memory usage.

~~~
Legogris
Counter-example:
[https://smaccmpilot.org/software/index.html](https://smaccmpilot.org/software/index.html)

Also, a Haskell DSL for automotive control systems:
[http://tomahawkins.org/](http://tomahawkins.org/)

~~~
krapht
That isn't a counter-example unless you're taking his post literally. In which
case I claim that Python is a system programming language because one time
somebody somewhere wrote a driver in it.

------
dmitriid
> Haskell, at its core, is simple: it is just a polymorphic lambda calculus
> with lazy evaluation plus algebraic data types and type classes.

This can potentially unseat the current reigning champ of Haskell-related
statements which is “A monad is just a monoid in the category of endofunctors,
what's the problem?”

~~~
Sharlin
To be fair, at least that's computer science speak rather than category theory
speak.

~~~
asplake
What body of knowledge defines "type classes"? I ask out of relative
ignorance, kinda saw it as Haskell thing

~~~
Sharlin
The term "type class" is original to Haskell, I think, but the concept
(constrained parametric polymorphism) is fairly standard programming language
theory.

------
anonnybonny
There is a persistent myth that "A Haskell program that compiles is correct by
definition"

Indeed it is "correct" in the sense that it has a very high likelihood (maybe
90% ?) of doing what the programmer intended (Unlike say C or C++ where the
probability drops to about 50% for beginners)

However, that is a poor definition of "correct". While it's true that a lot of
huge consequences have occurred due to stupid errors like buffer overflows
(unintended bug), there are equally disastrous bugs caused by intent - i.e.
the programmer had no idea the code could fail.

The biggest challenge to software development is not the "How can I avoid
silly mistakes?" but rather "How do I capture this extremely complicated real
world semantics in code?" and languages can't really solve that

~~~
dmitriid
I sometimes phrase it as “yeah, your program is correct because it’s type-
checked, but who has checked that your types are correct”?

With dependent types some of the logic will be coded in types, and we would
need to check _those_.

~~~
grumdan
At least now we are checking that two pieces of saying what we want a program
to do match up. It's less likely to get both the implementation and
specification (in the form of types) wrong. Whenever the program fails to
type-check, it will make you think about both bugs in the type and bugs in the
code, even if the latter is more likely.

~~~
dmitriid
Types can’t encode the specification fully, or require a lot of work to encode
a specification (so most people will skip this work).

The simplest example is very common banking and e-commerce logic which is
basically a series of checks in the form:

if <some piece of data retrieved at runtime at that particular moment> is
consistent with <multiple other pieces whose number and relevance depends on
that data retrieved at runtime from potentially multiple sources>.

------
p4bl0
What about F*? It has dependant types, and is already in use in production at
some places (there is a project of making a proved TLS implementation with it
and it is quite advanced iirc).

Also, it is somewhat based on F# which is based on OCaml so I would bet that
performance wise it can beat Haskell.

[https://www.fstar-lang.org/](https://www.fstar-lang.org/)

~~~
carlmr
I would also say OCaml and F# style is also less foreign than Haskell to most
newcomers. It's almost like (type-hinted) python + match expressions + pipes.

------
whateveracct
There are a lot of exciting new things on the medium-term horizon for Haskell.
-XDependentTypes, -XLinearTypes, and a new pluggable concurrent (latency-
optimized) GC

~~~
verttii
Can you link to more info about that latency-optimized GC?

~~~
mjh2539
Discussion here:
[https://www.reddit.com/r/haskell/comments/cp21wg/shorter_gc_...](https://www.reddit.com/r/haskell/comments/cp21wg/shorter_gc_pauses_coming_to_ghc/)

------
epiphone
How come none of the mainstream static type systems support dependent types?
On paper they seem like such a neat idea for defining constraints and
preventing bugs. I heard Scala and Haskell have optional/partial support for
them, so I suppose it takes a pretty sophisticated type system to begin with?

~~~
Sharlin
To implement dependent types, your type system must go much deeper into the
theorem prover territory than is normal in mainstream imperative programming
languages (whose type systems are often more or less ad-hoc rather than based
on rigorous theory). Furthermore, dependent types break the fundamental
property that the relationship between types and terms is one-directional:
types constrain terms but not the other way around.

As one example, C++ has had non-type template parameters (types parameterized
by values) for a long time, but only for compile-time constants. AFAIK there
have been no formal proposals to introduce full-blown dependent types, rather
the focus has been on improving the facilities for compile-time term-level
computation.

------
veonik
I spent a long winter understanding dependent types followed by a short summer
forgetting everything.

Anyway, I'm here for the koolaid.

~~~
bingerman
I'm curious which language/approach did you take. I recently picked Idris with
the book Type-driven development in Idris and so far I'm enjoying the ride (I
have some experience in functional programming but not in Haskell).

------
couchand
Previous discussion:
[https://news.ycombinator.com/item?id=18703245](https://news.ycombinator.com/item?id=18703245)

------
ErotemeObelus
Err... the problem with this is that the size of a list/array is sometimes
determined at runtime. This means that if you use dependent types, you can't
determine whether a program is correctly typed until you run the code.

~~~
rq1
Not at all. The verification is symbolic.

~~~
ErotemeObelus
Let's say we're creating an array of strings separated by newlines obtained
from reading a file. Call this arr[n] where n is determined at runtime.

Now let's have String index(arr:int[], i:(Fin(arr):int)), where FinInt(arr) is
a dependent type that is an integer between zero and the length of the array.
As the code is compiling, we have this

int i = 24951; String str = index(f, i);

The file exists in the future after the compilation. If the future file has at
least 24951+1 lines, then the compiler can verify that i < n and that i is
correctly typed. But if the file is short, then the type of i is incorrect. So
at compile-type the type of i is indeterminate.

~~~
aeneasmackenzie
you get a compile time error saying f isn't known to have enough entries, and
you resolve it by adding a decidable check that produces a proof that it does.
(the check at runtime is just >)

------
gridlockd

        bullshit :: Haskell -> DependentTypes -> Sunglasses -> Blockchain

------
auslander
Lisp is like a religion. Once you get it, you cannot help yourself spreading
the Word.

------
nudpiedo
> Haskell, at its core, is simple: it is just a polymorphic lambda calculus
> with lazy evaluation plus algebraic data types and type classes.

No, it’s not, if you can say this sentence without laugh at it, it is because
you developed a deep understanding about all the core concepts, meanings,
semantics and most probably now you know how to exploit them for their
appropriate usage to solve even more complex problems.

~~~
Certhas
It's simple as opposed to complex, not simple as opposed to difficult.

~~~
nudpiedo
Three different areas of computer science, highly related to mathematics AND
interrelated. Don’t you think these are not just difficult but also complex
since all their rules are combined and compose?

~~~
Certhas
I would put it like this: Haskell derives from (relatively) simple
axioms/concepts that have a rich and complex interplay.

~~~
nudpiedo
And such rich complex interplay isn’t exactly what makes things complex?
Interplay plays as synonym for complex in that sentence. Any complex system
could be broke down in simple axioms and some concepts and still be complex.

By the way I got severely downvoted for an opinion on what is complex and you
were the only one open for speak. Thank you for making a better HN somehow.

~~~
Certhas
"Any complex system could be broke down in simple axioms and some concepts."

This is the crux, I don't think that's generally speaking true. I don't think
Java and C++ could be broken down into a few simple axioms and concepts in the
way Haskell can.

Think design by committee, where you often end up with hundreds of concepts
and behaviours.

Happy to talk.

~~~
nudpiedo
Java can do it for sure (at least it was originally thought to be this way).
With C++ I think you really found the exception that makes the rule since it
was built by adding features on top of features without end or direction. I
can agree that there are systems which behave with more elegance than others,
with fewer exceptions and principles and more regular moving parts, with
reciprocal rules or consistent approaches, for example Java on its origins
what sort of simple. Now it is more a mess than ever before.

------
cryptica
Functional programming was a fad and it's going out of fashion (again).

It gives you rerefential transparency at the expense of real modularity.

Real modularity can only be achieved through proper separation of concerns.
The best way to achieve that is by categorizing system state and system logic
and allowing chunks of logic to be colocated with the relevant parts of the
system state. This is what OOP excels at.

With functional programming, you cannot colocate module logic with module
state so you often end up with a large monolithic state store on one side of
the system and logic which is completely decoupled and sitting on the opposite
side of the system... Then the challenge is to apply functions to various
relevant parts of the state to transform it in a referentially transparent
way.

The modularity problem then comes when you need to make a change to the
schema/structure of the state; whenever you change the structure of the state,
you need you need to search through your logic to identify all the parts of
the logic which depend on that state and update them to work with the new
structure. This is a massive problem and leads to what OOP developers call
spaghetti code.

~~~
moomin
I’m guessing you’ve never done any Haskell programming. I don’t know why you
think FP people don’t practice separation of concerns (they do), why you can’t
put state and the functions that manipulate that state in the same file (you
can) or that a type-checker can’t spot all the places you need to modify when
you change a data structure (it does).

Also, you seem unaware of the real restrictions on modularity caused by eager
evaluation.

~~~
cryptica
In that case it is not pure functional programming because you are colocating
state with logic. Functions in that file can mutate the state within that
file; this means that it's only functionally transparent within that file but
not from the outside.

If you need to store state inside of that file, then it means that you are
using it to affect the behaviour of functions which are exposed to the
outside... Also, this approach would be sub-par because you would be
constrained to singleton modules.

What you described sounds like a Haskell programming anti-pattern. It sounds
like a feature which was hacked on top to allow Haskell to emulate OOP
languages without actually doing OOP.

Also, lazy evaluation is a separate topic. OOP languages also support lazy
evaluation.

~~~
Legogris
What source code files you happen to put declarations and definitions in has
nothing to do with functional purity.

Think of it this way:

An array append function which adds an element to the input array is not
purely functional.

An array append function which returns a new array with the new element
appended is purely functional.

The same function applied to the same input will always return the same value,
and won't cause that to break for any other functions.

Of course, this makes things such as IO tricky and yo ideally want to minimize
and isolate the places where this happens on a lower level.

Haskell solves this with monads, which we have seen is a big hurdle for many
developers. I personally think Idris's effect system[0] is a lot easier to
deal with, but would be curious to see other approaches.

[0]: [http://docs.idris-
lang.org/en/latest/effects/index.html](http://docs.idris-
lang.org/en/latest/effects/index.html)

~~~
cryptica
>> An array append function which returns a new array with the new element
appended is purely functional.

>> The same function applied to the same input will always return the same
value, and won't cause that to break for any other functions.

I think this is the essence of why a lot of developers use functional
programming but I think that in this case functional programming is not a
solution to the real problem which is an architectural problem.

If the relationship between different components in your system is clear and
hierarchichal, you should never need to keep the same data/array in two
places. You can come up with simple abstractions so that you only ever have
one source of truth for each kind of state. That means you can read it
anywhere but only write to it in one place.

I think that functional programming cannot solve this root problem, you will
just end up with different symptoms. For example, instead of mutating a single
array in unexpected ways, in FP, if you have multiple sources of truth, you
might end up with multiple conflicting copies of the same data. Same root
problem and it's just as difficult to identify and debug as in OOP, just
different symptom.

