
Dependent Haskell - Serokell
https://serokell.io/blog/2018/12/17/why-dependent-haskell
======
tomp
I fundamentally disagree with the author's thesis. I don't think _dependent_
types are the future of development, but instead I'd bet on _refined_ types
(or contracts - see my experiment [0], Liquid Haskell [1] by Ranjit Jhala, and
languages Dafny [2] and F* [3] developed by Microsoft on top of Z3 SMT theorem
prover [4]), where constraints are explicit yet simple, and proven
automatically by an automated theorem prover.

    
    
      lookup : (a : array) -> (n : int if 0 <= n < length(a)) -> whatever
    

[0] [https://github.com/tomprimozic/type-
systems/tree/master/refi...](https://github.com/tomprimozic/type-
systems/tree/master/refined_types)

[1] [https://ucsd-progsys.github.io/liquidhaskell-blog/](https://ucsd-
progsys.github.io/liquidhaskell-blog/)

[2]
[https://rise4fun.com/Dafny/tutorial/guide](https://rise4fun.com/Dafny/tutorial/guide)

[3] [https://fstar-lang.org/tutorial](https://fstar-lang.org/tutorial) \- see
e.g. section 3 "First proofs about functions on integers"

[4] [https://github.com/Z3Prover/z3](https://github.com/Z3Prover/z3)

~~~
tom_mellior
> I don't think dependent types are the future of development, but instead I'd
> bet on refined types

> ...

> lookup : (a : array) -> (n : int if 0 <= n < length(a)) -> whatever

[https://en.wikipedia.org/wiki/Dependent_type](https://en.wikipedia.org/wiki/Dependent_type):
"In computer science and logic, a dependent type is a type whose definition
depends on a value."

In your example, you are using a dependent type: The type of n depends on the
value a. So your refined types _are_ dependent types, but possibly restricted
in some way. Can you explain the restrictions you have in mind to clarify (and
motivate) the relationship between full dependent types and your refined ones?
Is it something purely syntactic like "linear arithmetic only" (as I believe
was the case for Liquid Types, at least originally) to ensure decidability?

Edit: Even the very first sentence of the very first paper about Liquid Types
(the base of Liquid Haskell) talks about dependent types: "We present
Logically Qualified Data Types, abbreviated to Liquid Types, a system [...] to
automatically infer _dependent types_ precise enough to prove a variety of
safety properties." (emphasis mine)

~~~
tomp
Right, the difference is to some degree syntactic, but then, so is most
programming ("it's just math").

The most significant difference, as it appears to me (note that I've done only
a bit of research into refined types, and practically none into dependent
types), is that _dependent types_ means building top-down whereas _refined
types_ means building bottom-up.

Dependent types: _everything_ is proved, by hand, by the user, who has to
build up the whole program from basic blocks. Usually the "foundation" is
taken to be some kind of type theory, either Martin-Löf Type Theory or more
recently Homotopy Type Theory, which has some complexities (e.g. multiple
types of equality) and limitations (e.g. languages cannot be Turing complete)
that I don't really understand. "Pi types" abound. I'm not sure how much of
this description really is fundamental about Type Theory, and how much is just
due to the specifics of the implementations I've seen (Coq, Agda, Idris).

Refined types: start with a program and add some assertions about the values
(and possibly functions, but this can then get more complicated) in the
program. The idea is that many of these can ideally be proven by automated SMT
solvers, which support some "theories" (e.g. natural numbers, real numbers,
bitfields (i.e. machine integers and floating point numbers), algebraic
datatypes) and can prove some statements in these theories. As you mentioned,
"linear arithmetics", along with "real number algebra", are complete and
decidable theories, so in theory you should be able to prove anything about
them, but I'm guessing that in practice you'd still want some timeout
parameter on your SMT solver (like in type systems, where there are
exponential edge cases, that thankfully don't arise often in practice), but
they aren't very useful, so hopefully you can prove more than that, using the
heuristics embedded into SMT solvers (e.g. you should be able to prove many
non-linear properties of two-dimensional loops by lifting integers into
reals). If the solver is unable to solve something, the programmer can still
add hints (but this quickly gets complicated).

A concrete difference would be, in dependently-typed language, you define a
list type as

    
    
      type List a n =
        | Nil : List 'a 0
        | Const : a -> List a n -> List a (n + 1)
    

in a language with refined types, you can add _length_ afterwards:

    
    
      type List a = <... whatever ...>
      property length : List a -> Nat
    

And then the type checker/SMT solver will treat `length` as an _uninterpreted
function_ that always maps the same array to the same integer, and use it to
prove things.

Not sure if this makes a lot of sense, I haven't looked into this in a while
and am also not aware of the most recent research in these fields. Hopefully
someone can explain better!

~~~
tom_mellior
OK, I think I see where you're coming from. I don't think the distinction you
make is a useful one.

> A concrete difference would be, in dependently-typed language, you define a
> list type as [...]

Coq is a dependently typed language, and while it has a definition like this
for some thing (called vector, maybe? I forget), the definition for lists in
the standard library is not dependent, and length is a separate function. You
can still write a function like

    
    
        Fixpoint lookup (xs: list a) (idx: {n: nat | n < length xs}): a := ...
    

I don't know how much experience you have with dependently typed languages,
but it looks like you might have misunderstood tutorials showing that one
_can_ define a dependent list type as saying that one _must_ define lists
dependently?

> And then the type checker/SMT solver will treat `length` as an uninterpreted
> function that always maps the same array to the same integer, and use it to
> prove things.

Treating it as uninterpreted is quite weak. It allows you to prove simple
callers of the lookup function, but not the implementation. Also, you would
not be able to prove something like this:

    
    
        if idx < length xs then
            lookup (Cons foo xs) (idx + 1)
        else
            ...
    

This is admittedly a silly example, but it _should_ be provable. But you can
only do that if you can reason about the relationship between length and Cons.

As for automating proofs, I very very much agree that we need a lot more
automation. But if that means crippling our theories to an extend that they
are no longer expressive enough to verify our software, not much is won.

~~~
tomp
Cool, very interesting. I need to look into Coq again! Maybe it's closer to
what I want than I thought!

------
millstone
This read like a troll piece. Every line rubbed me in some wrong way until I
couldn't take it any more.

> Haskell, at its core, is simple: it is just a polymorphic lambda calculus
> with lazy evaluation plus algebraic data types and type classes. This
> happens to be just the right combination of features to allow us to write
> clean, maintainable code that also runs fast.

This is an explainabrag which is also wrong: languages without these features
can also have "clean, maintainable code" and nothing about this list implies
"runs fast."

> Memory unsafe languages, such as C, lead to the worst sort of bugs and
> security vulnerabilities (buffer overflows and memory leaks).

Is a memory leak meant to be the "worst sort of bug" or a "security
vulnerability?"

The implication is that memory-safe languages do not have memory leaks, which
is completely false. Haskell is particularly prone to "space leaks" due to its
laziness.

> Memory safe languages form two groups: the ones that rely on a garbage
> collector, and Rust.

What a profoundly dismissive attitude. Why bring up Cyclone and not, say,
Swift? Or is Swift meant to be included in "garbage collected languages?"

> Dynamically typed (or, rather, unityped)

No these are not the same and this is an absurdly wrong conflation. Dart,
Objective-C, TypeScript, and others are dynamically typed languages with
static type checking. You can't "or rather" this distinction away.

The author rushes past real-world facts to get to the architecture-astronaut
rocket ship, solving type-theoretical problems and pretending it's an
engineering exercise. Blast off, I guess.

~~~
pzone
That "explainabrag" pissed me off too. I feel like that kind of thing is super
typical among Haskell bloggers.

~~~
peteretep
At first I thought it was a joke along the lines of "A monad is just a monoid
in the category of endofunctors, what's the problem?", but no, it seems to be
being said non-ironically.

~~~
wtetzner
How is this the same thing?

\- polymorphic lambda calculus: Lambda calculus with generics

\- lazy evaluation: Well, lazy evaluation. Not sure how else to describe this

\- algebraic data types: Again, how else to describe this? Put in a full
description of what algebraic data types are?

\- type classes: Haskell has type classes.

I really don't see what the problem is. If you're coming in to an article
about dependent types, and you don't know what these things are, I think don't
think the reasonable response is for the author to explain all of it to the
reader. This isn't a beginner's introduction to Haskell.

------
wtracy
I really do think Haskell is a wonderful tool, but:

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

In a different context, I would have interpreted that as a sarcastic parody of
Haskell evangelists.

Really, I feel like everyone who is going to read that article will either
already know that, or will have no idea what that sentence means.

~~~
hardwaresofton
I was almost 100% certain it was a joke until I read the paragraph that quote
is from.

I want to reassure you that most Haskell evangelists (myself included) that
say stuff like that usually mean it as a joke. Most people who seek to
evangelize haskell do not lose sight of the fact that it's pretty daunting at
first and has a relatively steep learning curve.

That said, Haskell _is_ simple, but it's simple in an unintuitive way (as most
other languages obscure the ideas). For example I'd argue that when people
when people first hear "enum", what they really want is a sum type (i.e. `data
Something = OneThing {...} | TheOther {...}`) and _not_ what you get in most
languages which is enums-that-are-just-named-numbers or enums-that-are-just-
named-strings.

Most languages are coming around to the way haskell/other ML languages view
the world however:

\- non-nullable + option types \- function composition (a bunch of languages
get stuck in the filter/map phase but never get to the) \- typeclasses + data
types over classes + interfaces/abstract classes \- pattern matching \- Monads
\- The Free(R) Monad and attempts to make programs more like state machines
and encode it at the type level

I'm probably preaching to the choir in this thread but Haskell's type system
is like the mercedes of production-ready languages these days -- eventually
the features trickle down to other languages. There are more advanced options
out there like Idris (which already has good dependent type support) but it's
going to take a while for any of them to get the support and ecosystem haskell
fought hard for over all these years.

One highlight of Haskell's malleability and flexibility is the work around
linear types in Haskell[0] -- they basically give you rust-like semantics
(reference tracking for compile-time "automatic" memory management/etc)
without having to rewrite haskell from scratch. Turns out you can classify
rust's main feature under the super generic problem of "ascrib[ing] more
precise types". Turns out with a good enough type system, and lots of
patience/determination, you can solve a lot of common software issues at the
type level.

As the famous saying goes, the future _is_ here, it's just not evenly
distributed.

[0]:
[https://ghc.haskell.org/trac/ghc/wiki/LinearTypes](https://ghc.haskell.org/trac/ghc/wiki/LinearTypes)

~~~
atilaneves
> Haskell is simple

That it might be, but it sure as hell isn't easy. Brainfuck is simple, but
nobody would choose to write a real project in it.

Most programmers struggle trying to understand what a monad is. That's not
easy.

The free monad is not easy.

Monad transformers are not easy.

Understanding foldable/traversable/arrows/applicate is not east.

Lens is/are not easy.

I have personally worked with _dozens_ of programmers that would never be able
to write "proper" Haskell.

~~~
agentultra
Easy for whom?

Finding Haskell difficult is nothing to be ashamed of. However it's going to
appear more difficult than it ought to be if you're approaching it as an
experienced programmer with strong opinions. It can be humbling to realize
that there's a whole branch of programming you're completely new to and know
little about. It's going to be difficult to climb that mountain and you will
feel like a beginner again and that's okay.

It's worth the effort!

~~~
atilaneves
> Easy for whom?

Nearly everyone I have met and/or worked with.

> Finding Haskell difficult is nothing to be ashamed of

Agreed.

> It can be humbling to realize that there's a whole branch of programming
> you're completely new to and know little about.

Agreed.

> It's worth the effort!

To learn it? Sure. To use it? I disagree. I was more unproductive with Haskell
than I've ever been with C, which is usually the language I love to bash on. I
can look up opcodes and write assembly faster. "Ah, but with experience!" \- I
don't see why I'd bother.

I write mostly functional code in any language I'm working in as it is. The
functional parts of Haskell don't really have anything for me, at least not to
compensate the pain of not getting anything done.

Take lens, for instance. It's an incredibly complicated solution to a problem
that AFAIK only plagues Haskell! I'd love to know of any exceptions.

Writing networking code was an exercise in frustration where I had to try
multiple libraries and faff about with changing types from eager to lazy
bytestrings..

I'm glad I bothered to try and learn Haskell. I just don't see me ever using
it in production, let alone try to hire anyone who knows it. It's hard enough
finding Python programmers who know what generator expressions are.

~~~
agentultra
If we're only talking experiences here then I can say that I've written plenty
of networking code in C. I grew up on C. I have 15+ years' experiencein C-like
languages across the gamut; more if you count the years when I was a youth and
hacking together games and homework assignments for fun.

I've definitely felt the pain you're describing and have even thought of lens
in the way you ascribe. How can anyone be productive in a language where
logging is so difficult to add? I can add logging to a Python application in 3
lines. And as you say... converting from lazy to strict from text to
bytestring to string depending on the library. So annoying! Nothing like those
clean examples they show you in the tutorials!

Every time I went down that rabbit hole was because I was getting frustrated
with feeling like a beginner again. What was I getting from Haskell for all of
this hassle? Why were these simple things in other languages so hard? What's
the benefit?

Peace of mind.

It's easier to get started with a C or Python or Javascript program. Partly
because of experience. Partly because those languages do nothing to isolate
side effects, constrain mutation, or check my code in any effective way. I
stick to a particular style (usually functional), I write tests first, and I
usually end up paying for that easy start later on in the project when,
despite our intentions and efforts; side effects, mutation, and plain old type
errors creep in. What I gain in efficiency early on in the project I pay for
later with interest. The interest rate is only compounded when we start adding
team members.

My experience with Haskell has been that it is frustrating and sometimes
slower to get started with, especially when I was first starting out, but that
it has been worth the effort in the long run. Down the road when my project
started to grow I was better equipped to manage the complexity because I had a
type system that kept me honest and guided me towards strong designs. I had a
tool that would ensure that only the parts I was really sure about could
perform IO actions or use shared mutable state. And best of all I could not
touch a module for months and when I came back to it there's a good chance I
could understand what it was doing, make the change I needed to make, refactor
it, and push it to production without any worries.

I think the human side of software development was the nicest feature of
working with Haskell. As I added new team members I didn't have to worry about
junior developers breaking the build as much. Training was much more straight
forward. The documentation was much easier to write as we could focus on the
higher-level designs and let the type system document the details. Testing was
more effective as we could focus on problems and tests that brought more value
to the business problems.

I still choose other languages for various reasons but Haskell has been worth
it in my experience. Painful but worth it.

------
tomkludy
Every discussion or article I have read on dependent types assumes that the
reader is a mathematician. I usually make it only a few paragraphs in before I
am completely lost.

Right now the only thing that seems clear is that dependent typing adds
significant mental burden on the programmer to more thoroughly specify types,
and to do so absolutely correctly. In exchange for that burden, there must be
practical (not theoretical) benefits but they do not come through clearly. All
of the examples I've seen are about "index of a list is guaranteed in bounds".
That is such a minor and infrequent bug in practice that it does not justify
the additional complexity. There must be more, that I'm just not seeing.

Is there a "Dependent types for non-mathematicians" article out there
somewhere, where I can learn about patterns and practical applications?

~~~
lmm
> Right now the only thing that seems clear is that dependent typing adds
> significant mental burden on the programmer to more thoroughly specify
> types, and to do so absolutely correctly.

I'd say that's backwards: rather a dependently typed language relaxes a huge
restriction that most programming languages have, that only certain things can
be used as types. Any program that's valid in language X is also valid in a
dependently typed version of language X (e.g. most Haskell functions translate
directly into Idris as long as they don't rely on laziness).

Dependent types make it easier to encode properties that you care about into
the type system, i.e. rather than the programmer having to get them right, the
compiler can check them for you. Far from burdening the programmer, it
lightens your mental load.

> All of the examples I've seen are about "index of a list is guaranteed in
> bounds". That is such a minor and infrequent bug in practice that it does
> not justify the additional complexity.

Most code is in terms of domain-specific things, and so the types you use are
also domain-specific. In my experience every code bug (as distinct from
"behaving as specified but not as intended" bugs) boils down to "we thought x,
but actually y" and can be avoided by making more precise use of a type
system. If you have bugs that hit production, you can probably avoid them with
better types. If you avoid production bugs by using tests, you can probably
replace most of those tests with types and they'll become more concise and
easier to maintain. But of course the specifics of what types to use will be
specific to your domain; examples like list indices are common because they're
one of those rare types that virtually everyone uses.

------
AnimalMuppet
Actual article title: "Why Dependent Haskell is the Future of Software
Development" \- which the author spends a few handwavey paragraphs
unconvincingly trying to show. After that, though, the article moves on to
_why_ to get there, and _how_ to do so, both of which are more interesting
than the attempt to justify the claim in the title.

------
incadenza
Does anybody care to explain dependent types? I’ve heard the term used in FP
conversations but not sure I get it.

~~~
huntie
Dependent types are types which depend on values. As an example, think of the
cons procedure: (List A, A) -> List A. It takes a List of A's and an A and
returns a List of A's. With dependent types you can write this as (List n A,
A) -> List n+1 A. This tells us that cons takes a List of A's with length n
and an A returns a List of A's with length n+1.

Edwin Brady shows off some examples in Idris here[1]. I thought the matrix
example was really impressive.

[1]
[https://www.youtube.com/watch?v=mOtKD7ml0NU](https://www.youtube.com/watch?v=mOtKD7ml0NU)

~~~
joe_the_user
When I see this definition, I scratch my head and wonder why this isn't a
different way to introduce object orientation, generics, c++ parameterized
templates and such.

~~~
logicchains
The key difference with C++ templates is that in C++ you can't have a type
parameterised by a value that's only know at runtime, e.g. a std::array<N>
where N is read from stdin. In a dependently typed language, you can. Object
orientation is a different matter entirely; in the sense it implies Java-style
class-based inheritance, it's almost in the opposite spirit to dependent
types, as such late-binding (virtual methods) means it's possible to call
methods such that the compiler has no idea which method will be called at
compile time, making it hard to reason about the code at compile time.

------
AzzieElbab
Sounds like commendable goal. Kind of like ATS without the pain

