
Types - ivank
https://gist.github.com/garybernhardt/122909856b570c5c457a6cd674795a9c?
======
seanwilson
Great overview article but I have a comment.

> In Idris, we can say "the add function takes two integers and returns an
> integer, but its first argument must be smaller than its second argument":

> If we try to call this function as add 2 1, where the first argument is
> larger than the second, then the compiler will reject the program at compile
> time.

> Haskell has no equivalent of the Idris type above, and Go has no equivalent
> of either the Idris type or the Haskell type. As a result, Idris can prevent
> many bugs that Haskell can't, and Haskell can prevent many bugs that Go
> can't. In both cases, we need additional type system features, which make
> the language more complex.

I really wish when people talk about dependently typed programming languages
(e.g. Idris, Coq, Agda, ATS), they would mention the effort involved to get
these compile time checks to happen.

These languages allow you to capture almost any program property you can think
of as a type. Instead of being limited to saying function F "returns a
number/list/string" like common type systems, you can capture arbitrarily
complex properties like "F returns an even number", "F returns a permutation
of the input list", "F always halts" or "F returns a proof of Fermat's Last
Theorem". You don't get this for free and you have to help the computer verify
these.

Proving F matches the type is arbitrary hard and is impossible to automated in
general (e.g. the halting problem tells us we cannot always tell if a function
halts). For example, consider the type "F returns a sorted permutation of the
input list", where F could be an implementation of bubble sort, quicksort,
merge sort, radix sort or something even more complex. Each algorithm will
require a different and potentially complex proof and most of the time the
programmer needs to help the computer find the proof. It's completely unlike
what people are used to from type systems and is an even more massive leap
than for someone moving from the type systems of e.g. C++/Java to Haskell.

I'm glad people are more aware of dependently typed languages but I feel the
immense jump in how practical they are to use is severely overlooked.

~~~
logicchains
>You don't get this for free and you have to help the computer verify these.

For library types, the most commonly used properties will have already been
proven; you wouldn't have to write a proof that "sort" sorts a list any more
than you'd have to write the sort function itself.

The computer can also be surprisingly good at proving these things in many
cases. E.g. I was playing with the proof assistant Isabelle recently, and I
tried to prove that given an int, a proof that twice that int is greater than
13, and a proof that four times that int is less than 29, then that int must
be 7. I thought I'd have to fiddle around with induction on Peano numbers and
the like, but nope, it could be proved with just a single "by arith".

~~~
catnaroek
> it could be proved with just a single "by arith".

Tactics are probably a usability improvement for mathematicians who are used
to proving everything by hand. But for programmers used to type inference,
they're a step backwards. Mathematicians typically prove much deeper results,
but they do so at a much slower rate than programmers write programs.

~~~
Ericson2314
Huh tactics are pretty great. Say exactly what you want and the computer
programs itself!

It's unfair to just compare development time between tactic-generated programs
in a dependent language with manually written programs in a non-depenendent
language. The end result in the dependent language is much more valuable.

~~~
catnaroek
I'm not comparing dependent types vs. no dependent types. I'm comparing
higher-order dependent types (Agda, Idris, Coq, etc.) vs. first-order
dependent types:
[https://news.ycombinator.com/item?id=12350147](https://news.ycombinator.com/item?id=12350147)

Seriously, Coq-style proof scripts are utterly unredable when they grow past a
certain size. The only way to understand them is to replay them, so that you
can see the intermediate hypotheses and goals.

~~~
mietek
I agree. Note that the Coq-style tactics-based approach is not the only
possibility.

In Agda, you are expected to write proofs in a functional style, similar to
Haskell programs. There is a small amount of integrated automation, which
helps you fill in holes in your proofs — however, the results are explicit
proof terms, inserted at the right place in your program.

Systems which behave in this fashion have the de Bruijn criterion, as
described in Geuvers (2009) “Proof assistants: History, ideas, and future”.

[https://www.dropbox.com/s/4mwtxojg7yqb365/Geuvers2009.pdf](https://www.dropbox.com/s/4mwtxojg7yqb365/Geuvers2009.pdf)

~~~
catnaroek
Thanks for the links (this one and the one from your other reply), I'm
checking them.

------
twblalock
The Idris example seems to need further explanation:

> In Idris, we can say "the add function takes two integers and returns an
> integer, but its first argument must be smaller than its second argument":

> add : (x : Nat) -> (y : Nat) -> {auto smaller : LT x y} -> Nat

> add x y = x + y

That's all well and good, if you know the values of x and y at compile time.
Consider a program that reads x and y from STDIN. The user could provide an x
that is equal to or larger than y (or could provide only one value, or values
that are not numbers). I see no way to deal with that except to throw a
runtime error. Is that what would happen?

~~~
_ibu9
The missing explanation is that `smaller` is a third argument to the function.
It's type is a proof that x <= y. Since it is in curly brackets with the auto
keyword, the compiler will fill in this proof in many cases, like when the
values are statically known.

In the case where the values are read from the external environment, first you
would have to compare them before calling the function. The comparator would
return either a proof that x <= y, or x > y. In the first case you plug that
value into the `smaller` argument, in the second case it's your responsibility
to signal whatever kind of application-specific error is appropriate (assuming
x > y is some kind of erroneous condition).

~~~
twblalock
What would happen if you did not compare them before calling the function, and
just passed them in such that x >= y?

~~~
icen
Nothing; it would return a partially applied function that waits for a proof
that `x < y` before continuing. (Trying to use that function as an integer
would be a type error).

The key here is that `add` is _not_ a function `Nat -> Nat -> Nat`, it is a
function `(a : Nat) -> (b : Nat) -> LT a b -> Nat`. There are just some
compiler features that allow you to avoid having to write out the full `LT a
b` proof each time you want to add things!

~~~
chriswarbo
What's even more interesting is the structure of "LT a b". Let's assume that
Nat has the common Peano arithmetic structure:

    
    
        data Nat : Type where
          Zero : Nat
          Succ : Nat -> Nat
    

Here "Zero" represents zero and "Succ" represents "one more than". Hence "Succ
(Succ (Succ Zero))" is one more than one more than one more than zero, AKA
three.

Given two such values "x" and "y", how on Earth can we prove that "x < y"?
What would that even look like?

Well, there's a really obvious case: we know that "x < Succ x", so we can
define a value "Obv" which represents this:

    
    
        Obv : (x : Nat) -> LT x (Succ x)
    

Notice that we don't write "y" explicitly, since it can be written in terms of
"x".

What about those cases where "x" and "y" differ by some other amount? We could
define values for "x < Succ (Succ x)", "x < "Succ (Succ (Succ x))", and so on,
but that's clearly redundant and inconvenient. Instead, we just need to spot
that all of these fit the pattern "x < Succ y", where "x < y". This lets us
argue by induction: if "x < y", then "x < Succ y", which we can represent
using a value "Ind" like this:

    
    
        Ind : (x : Nat) -> (y : Nat) -> LT x y -> LT x (Succ y)
    

This is enough to prove that "x" is less than any number greater than "x". In
fact, we don't need to give the values of "x" and "y" explicitly, as they
appear in the "LT x y" types and can hence be inferred. In languages like
Idris we indicate inferrable parameters using braces, hence our "LT" type
looks something like this:

    
    
        data LT : Nat -> Nat -> Type where
          Obv : {x : Nat} -> LT x (Succ x)
          Ind : {x : Nat} -> {y : Nat} -> LT x y -> LT x (Succ y)
    

Now, this looks familiar. Compare it to the definition of "Nat": we have two
constructors, one of which ("Zero"/"Obv") can be written on its own, whilst
the other ("Succ"/"Ind") is recursive, requiring an argument containing the
same constructors.

Values of type Nat include:

    
    
                   Zero  : Nat
              Succ Zero  : Nat
        Succ (Succ Zero) : Nat
    

And so on, whilst values of "LT x y" include:

    
    
                 Obv  : LT x (Succ x)
             Ind Obv  : LT x (Succ (Succ x))
        Ind (Ind Obv) : LT x (Succ (Succ (Succ x)))
    

Although LT contains more static information than Nat, it actually follows
exactly the same structure. What does this mean? Values of type "LT x y" _are
numbers_ ; in particular they're the _difference between "x" and "y"_!

In the case of "LT", these numbers start counting from one (since one number
is not less than another if their difference is zero). If we define a similar
type for "x <= y" it would count from zero, and the same can be done for ">"
and ">=".

These types are actually really useful. They're also closely related to linked
lists, vectors, etc. although they store dynamic information as well as
static.

I assume this is all old hat to those with mathematical training, but I found
it interesting enough to write about at
[http://chriswarbo.net/blog/2014-12-04-Nat_like_types.html](http://chriswarbo.net/blog/2014-12-04-Nat_like_types.html)

------
naasking
Decent overview of many concepts, but the opening line isn't strictly true:

> A type is a collection of possible values.

A type is a proposition, and "This binding has one of these possible values"
is merely one type of proposition. So a type is much more powerful than a
simple set-based interpretation, even though this is how most people think
about it.

For instance, in Haskell you can encode a region calculus that ensures prompt
reclamation of file handles and other scarce resources [1]. The relations
between nested regions to ensure correctness seems awkward to interpret as a
set, but it's straightforward if you just think of types as a relation.

[1] [http://okmij.org/ftp/Haskell/regions.html#light-
weight](http://okmij.org/ftp/Haskell/regions.html#light-weight)

~~~
rntz
(Static) types may be seen as propositions, but this is just one
interpretation of them. We can also interpret types as sets (although this
doesn't work out very well except in very simple languages), as domains (most
commonly), as cpos, as relations over closed terms (PERs), and so forth.

The types-as-propositions interpretation is a particularly useful one because
it lets us take ideas from the field of formal logic and apply them to
programming. But it is only _one_ interpretation, and _not_ a definition of
what types are.

I don't think it's possible to give "type" a good, short definition. For me
John Reynolds' definition comes closest: "Type structure is a syntactic
discipline for enforcing levels of abstraction." But that's rather a mouthful,
and not a good introduction to what types are.

> A type is a proposition, and "This binding has one of these possible values"
> is merely one type of proposition.

The type that an expression or variable has does _not_ correspond to the
proposition that that expression has a certain set of possible values! You're
mixing the object-level (the language & its types) with the meta-level
(assertions about the language). This is a total misunderstanding of Curry-
Howard. Rather, the values an expression might take on can be interpreted as
_proofs_ of the corresponding proposition.

~~~
naasking
> (Static) types may be seen as propositions, but this is just one
> interpretation of them. We can also interpret types as sets (although this
> doesn't work out very well except in very simple languages), as domains
> (most commonly), as cpos, as relations over closed terms (PERs), and so
> forth.

Right, I wasn't advancing the propositional definition as the only one, merely
as one that sufficiently captures the generality, because the article's
current definition of types-as-sets is insufficiently general.

~~~
catnaroek
Under HoTT's take on propositions as “types where any two inhabitants are
equal”, “types as propositions” is even less general than “types as sets”.

As I stated in a comment on that gist, what types denote varies from one type
system to another:
[https://gist.github.com/garybernhardt/122909856b570c5c457a6c...](https://gist.github.com/garybernhardt/122909856b570c5c457a6cd674795a9c?#gistcomment-1856634)

~~~
naasking
No one except mathematicians and type theorists are using HoTT, where every
programmer in existence is using first-order and sometimes second-order logic.
I think it should be obvious which is more appropriate if this is supposed to
be an introduction.

The idea that types express propositions in first- or second-order logic about
a program one is writing is sufficiently general for every programmer to
understand the proper scope of type checking. Types-as-sets is simply false as
an introductory concept.

~~~
catnaroek
> No one except mathematicians and type theorists are using HoTT,

Right.

> where every programmer in existence is using first-order and sometimes
> second-order logic.

I'm not so sure about this one. Perhaps by “programmer” you mean “every
Haskell programmer”? Even then I'm still not sure. Are you counting “let's
approximate `forall x. P(x)` with the large but finite conjunction `P(a) /\
P(b) /\ P(c) ...`” as “using first-order logic”?

~~~
naasking
I mean pretty much every programmer, even C programmers. Simple business logic
is rife with such propositions. Note that I didn't say they use first-order
logic to encode invariants in types or anything, but they obviously use it
everyday to write programs

Haskell programmers also use higher order logic, which is also common now in
Java and C#.

~~~
catnaroek
> Note that I didn't say they use first-order logic to encode invariants in
> types or anything, but they obviously use it everyday to write programs

I wasn't talking about types either. Most programmers I've talked to simply
can't use formal logic.

~~~
naasking
Hmm, I don't think you're being very charitable. That these programmers can
write working programs suggest they sufficiently understand disjunction,
conjunction, negation, conditionals and quantification.

Perhaps they have trouble abstracting the semantics from their preferred
language and/or syntax, but that doesn't mean they don't grasp their meaning.

~~~
catnaroek
I offer this thread as evidence:
[https://news.ycombinator.com/item?id=12342583](https://news.ycombinator.com/item?id=12342583)

~~~
eli_gottlieb
Hey, it's not the fault of the programming masses that some evil motherfucker
conflated implementation extension (inheritance) and subtyping.

Subtyping = "A <: B, therefore I can start with an A, forget the details
irrelevant to B, and just use it as a B."

Inheritance = "A inherits B, if I try to actually use it as a B, it promises
to work. However, it might actually blow up in my face, because it was keeping
its extra details secret from me and promising they wouldn't blow up."

~~~
catnaroek
> programming masses, some evil (bleep)

Too harsh. Logic is hard for humans. Evolution has trained us to take lots of
mental shortcuts, not to be perfect calculating machines. Even the most
technically competent people rely on their intuition. What differentiates them
from the rest of us is that they have trained their intuition to make it more
reliable [0].

It's the job of a language designer to arrange things so that intuition
doesn't mislead programmers regarding the meaning of language features. Before
proposing a new feature, the designer must anticipate how programmers will
think about code that uses this feature, and there better not be a mismatch
between what programmers think the code means and what the code actually
means.

> A inherits B, if I try to actually use it as a B, it promises to work.

There's nothing wrong with implementation inheritance per se [1]. It addresses
a very common use case: you have a thing Foo, and you want to produce another
thing Bar that's similar to Foo, with minor changes here and there. This
describes accurately how many things work in real life. However, you can't
assume that Bar can always be used where Foo is expected. Those “minor changes
here and there” might have rendered Bar incompatible with Foo. Hence,
“inheritance is not subtyping”. As stated above, it's the job of a language
designer to anticipate this.

[0] [https://terrytao.wordpress.com/career-
advice/there%E2%80%99s...](https://terrytao.wordpress.com/career-
advice/there%E2%80%99s-more-to-mathematics-than-rigour-and-proofs/)

[1] Other than the lack of mathematical elegance, but few things are
mathematically elegant in life.

~~~
eli_gottlieb
>Hence, “inheritance is not subtyping”. As stated above, it's the job of a
language designer to anticipate this.

Yes, it is their job, by _not allowing subclasses to be used as subtypes._

~~~
catnaroek
Some subclasses can be safely used as subtypes. For example, those that meet
the following two conditions:

(0) Not directly mutating superclass fields. (Reading them is fine.)

(1) Not overriding non-abstract methods of the superclass.

~~~
dragonwriter
If classes are types, subsclasses should be subtypes -- so the above
restrictions should just be applied to subclass relationships.

One could also have an non-subclassing mechanism of implementation sharing,
that might be declared something like this:

    
    
      class Related does (method1, method2) like Base; 
    

_or_

    
    
      class Related does all except (method3) like Base;

~~~
catnaroek
That syntax is too heavyweight (IMO). AFAICT, most of the time, when people
use inheritance, they don't want to define perfect subtypes. They just want to
reuse as much already implemented behavior as possible.

~~~
dragonwriter
> That syntax is too heavyweight (IMO).

I'd be fine with something more compact for the default case of "steal all
behavior except what is explicitly overridden", such as:

    
    
      class Related like Base;

~~~
catnaroek
(0) Do you have a use case for cherry-picking which methods are inherited? If
not, it seems like too much complexity for me.

(1) Do you think there would still be a use case for “extends”? If not, I'd
rather drop it as well.

~~~
dragonwriter
> Do you have a use case for cherry-picking which methods are inherited?

The most obvious is that it makes intent explicit in the case of multiple
similarity (I prefer "similarity" to "inheritance" for non-subtyping
implementation reuse.)

It might in some cases make sense to do this on an _interface_ rather than (or
in addition to) _single method_ level; there's probably a lot to work out in
the ergonomics of non-subtyping implementation reuse.

> Do you think there would still be a use case for “extends”?

Sure, classical subclassing (subtyping with implementation sharing) remains an
important use case. It might be desirable to statically verify certain
guarantees (some have been suggested in at least one subthread of this thread
by another poster) to assure that the subtyping is reasonable, but even
without such verification the use case remains important.

~~~
catnaroek
> The most obvious is that it makes intent explicit in the case of multiple
> similarity

Keep in mind that similarity is explicitly intended to be a quick-n-dirty form
of code reuse. If the programmer has enough time to refactor the code, he or
she should probably factor out the common parts, and then use proper
(subtyping) inheritance. In view of this, I don't think making similarity
overly complicated is a good idea.

> Sure, classical subclassing (...) remains an important use case. (...) even
> without such verification the use case remains important.

It's precisely the lack of such verification that destroys the guarantees you
can extract from Liskov's substitution principle.

~~~
dragonwriter
> If the programmer has enough time to refactor the code, he or she should
> probably factor out the common parts, and then use proper (subtyping)
> inheritance.

I do see the attraction of the idea that all implementation sharing ultimately
reflects something that can be expressed through a subtyping hierarchy
adhering to the LSP, but I'm not 100% convinced that it is the case. Absent
certainty on that point, I'd like to have a model of similarity that is
workable when viewed as an ultimate, rather than interim, model.

Also, I think that this kind of explicitness (with similar syntax) is
desirable for subtyping inheritance in languages that support multiple
inheritance, so it keeps similarity in line with inheritance, with the
distinction only in the relationship being expressed (so, while it may make
similarity more complex, it keeps the whole language more simple but
explicit.)

> It's precisely the lack of such verification that destroys the guarantees
> you can extract from Liskov's substitution principle.

I agree that static verification of sanity in subtyping inheritance reduces
the problem of LSP violations (as, frankly, does non-static verification, such
as testing frameworks using subtype relationships to automatically apply tests
for supertypes to subtypes.)

The degree and type of verification (as well as other details like
cherrypicking and support for multiple similarity) that is appropriate for
these type of things really depends on the language. Extending Perl or Python
(dynamic languages with multiple inheritance) to support similarity alongside
inheritance is probably going to favor different tradeoffs than extending Java
(static with single inheritance) to support similarity.

~~~
catnaroek
> I agree that static verification of sanity in subtyping inheritance reduces
> the problem of LSP violations

The LSP itself is never violated in a type-safe language. What's violated is
the guarantees that you expected the LSP to buy you. For instance, Java
doesn't promise about the meaning of non-final methods. If a programmer
assumes such guarantees, the problem is with the programmer, not Java. If he
wants such guarantees to hold, he has to use a different language.

------
junke

        1 + eval(read_from_the_network())
    

> If we get an integer, that expression is fine; if we get a string, it's not.
> We can't know what we'll get until we actually run, so we can't statically
> analyze the type.

> The unsatisfying solution used in practice is to give eval() the type Any,
> which is like Object in some OO languages or interface {} in Go: it's the
> type that can have any value. Values of type Any aren't constrained in any
> way, so this effectively removes the type system's ability to help us with
> code involving eval. Languages with both eval and a type system have to
> abandon type safety whenever eval is used.

No, you can give EVAL a returning type of any (T) but still make type
inference work.

    
    
        (+ 1 (eval (read)))
    

The meaning of a type in SBCL is: _if an expression evaluates without error,
then its type is ..._. In other words, the type of the above expression is
NUMBER.

There is also a NIL type (bottom) which represent the empty set of values. If
a function has a return type of NIL, it means that it does not return a value
normally. This is the case for the ERROR function, or the (LOOP) expression.

~~~
evincarofautumn
> if an expression evaluates without error, then its type is…

What’s curious is that this is true in every language with general
recursion/looping:

    
    
        int f(int n) {
            while (true) {}
            return n + 1;
        }
    

(In other words, the type of “f” isn’t the logical formula “a → a”, but rather
“a → ¬¬a”.)

The type of “eval” can also be fully static; it can just return a type that
you have to do case analysis or runtime type inspection on to use.

In Haskell, with a data type:

    
    
        data Value = Integer Int | Text String | ...
    
        eval :: String -> Value
    
        foo = do
          value <- read_from_the_network
          let result = eval value
          case result of
            Integer n -> return (1 + n)
            _ -> error "that was not an integer!"
    

Or with RTTI:

    
    
        eval :: String -> Dynamic
    
        foo = do
          value <- read_from_the_network
          let result = eval value
          case fromDynamic result of
            Just n -> return (1 + n)
            Nothing -> error "that was not an integer!"
    

“fromDynamic” has the type:

    
    
        Typeable a => Dynamic -> Maybe a
    

Which is to say “given a dynamic value, and its runtime type information, I
can either give you the value back with a static type, or nothing if I can’t
perform the downcast”—much like “dynamic_cast” on pointer types in C++.

~~~
junke
> What’s curious is that this is true in every language with general
> recursion/looping:

Yes, thanks for pointing that out.

> The type of “eval” can also be fully static; it can just return a type that
> you have to do case analysis or runtime type inspection on to use.

I'd say that's what type T stands for in Lisp, since you can then dynamically
dispatch on the actual type of the value.

------
wz1000

        add : (x : Nat) -> (y : Nat) -> {auto smaller : LT x y} -> Nat
        add x y = x + y
    
    

Actually, (GHC) Haskell can express something like this type if you turn on
the required extensions.

    
    
        data Nat = Z | S Z deriving (Eq, Ord, Show)
        data SNat (n :: Nat) where
            SZ :: SNat Z
            SS :: SNat n -> SNat (S n)
    
        type family Compare (a :: Nat) (b :: Nat) :: Ordering where
          Compare Z Z = EQ
          Compare (S a) Z = GT
          Compare Z (S a) = LT
          Compare (S a) (S b) = Compare a b
        
        type a < b = Compare a b ~ LT
    
        type family (+) (a :: Nat) (b :: Nat) :: Nat where
          Z + a = a
          a + Z = a
          (S a) + b = S (a + b)
        
        add :: (a < b) => SNat a -> SNat b -> SNat (a+b)
        add = ...

------
DSMan195276
Being someone who enjoys C (Though I readily admit the type system is
generally weak compared to the others on the list) there's a _little_
misinformation on this page that I think it worth clearing up - though I think
the majority of the information is still perfectly good:

C does not 'allow' you to do a lot of the things mentioned on this page, it
just doesn't generally carry around all the information to check and make sure
you don't. So the page is generally still right - it can be fairly easy to get
memory-unsafe things past the compiler - though whether you want to blame the
type-system or blame the compiler could be debated, probably a bit of both.

Reading past the ends of arrays is illegal, as is accessing a variable of one
type through a pointer to another type. Some of these things _will_ get
checked or assumed by the compiler (And thus break code that does these
things, and in some ways break them in subtle ways), but there's no guarantee
that the compiler actually knows you broke the rules. This tends to be the
main different from other languages like Java, where the compiler/run-time
_does_ carry around such information and thus can do such checks and does know
you when you broke the rules.

With that said, while there are some things I don't like about the design of
Rust overall, giving the compiler more information to be able to figure out
the above is definitely one thing they did right in comparison to C. The
problem C is approaching is that, even though compilers are getting smarter,
enough code was already written before that was the case to result in breaking
when you attempt to use 'smarter' compilers.

~~~
naasking
Since this articles is about types, any behaviour the compiler doesn't check
or doesn't generate a type error is "allowed".

~~~
DSMan195276
The issue with that idea is that there is more than one C compiler. Some of
them issue no warnings at all, and some of them (like `gcc`) will yell at you
and probably break your code if you attempt to do some of the things I
outlined.

I disagree with saying it is 'allowed' by the type system, because a smart C
compiler could throw out your code and refuse to compile it if you do such
things, and in doing so would still be within the C standard. Some work has
already been done on this front in `gcc` (And I would assume `clang`) - the
issue tends to be that by doing this, you end-up breaking some older code that
used to work fine. And since older compilers didn't enforce these rules,
people aren't as aware of them.

~~~
AnimalMuppet
In theory, languages have type systems. In practice, compilers do.

If there's more than one C compiler, and they behave differently on this, then
in practice, C has more than one type system.

------
titanomachy
Someone who has experience with both Agda and Idris, could you comment on
which is easier to get started with? Coming from a Haskell background, I'd
like to try my hand at writing more interesting constraints into my types. Any
experiences using these languages for (non-research) work?

I've played around with Coq a little and it certainly feels more like a proof
assistant than a programming language. It was fairly confusing and unfamiliar,
but interesting nonetheless. Something with slightly more traditional
ergonomics might be easier to pick up.

~~~
bjz_
I found Idris pretty easy to get started with - that's part of it's goal,
making dependent types easier to get started with. From there I think it would
be easier to go to Agda or Coq, but I haven't explored much there myself.

~~~
whateveracct
+1 for Idris. The tutorials were pretty solid for it too when I tried it, and
the mailing list was very helpful when I had questions [1]. There's even a WIP
book! [2]

[1] [https://groups.google.com/forum/#!forum/idris-
lang](https://groups.google.com/forum/#!forum/idris-lang)

[2] [https://www.manning.com/books/type-driven-development-
with-i...](https://www.manning.com/books/type-driven-development-with-idris)

~~~
bjz_
Their main docs are pretty good too: [http://docs.idris-
lang.org](http://docs.idris-lang.org)

------
arianvanp
> If we try to call this function as add 2 1, where the first argument is
> larger than the second, then the compiler will reject the program at compile
> time

This is not entirely true in general. auto is only a best bet. It might not be
able to prove something right and will fail. For example, if it has to recurse
deeper than 100 constructors. In this case, the user of the function has to
provide a proof himself.

See it as a bloom filter. if it doesn't fail, you're sure your function is
correctly called. if it does fail, it might still be correctly called but the
compiler just didn't have enough time to prove it. In that case a user needs
to convince the compiler that he is right.

~~~
gary_bernhardt
I don't see the problem here. I said that Idris will reject the program if the
proof fails. That's what you're saying too, unless I'm very confused. (It's
late, so maybe I am?)

~~~
arianvanp
Yes, but it might also reject correct programs. that's the point I'm trying to
make.

~~~
gary_bernhardt
That doesn't contradict the quote that you provided. The quote just says "the
compiler will reject the program at compile time [if there's no valid proof]",
which is true.

------
one-more-minute
If this topic interests you, Julia's type system is worth checking out [1].
Julia is unusual among dynamic languages not just for having a powerful type
system (including parametric types), but also for making it idiomatic to make
full use of types. Aside from the benefits to the programmer, this also allows
the compiler to get to Rust-level performance where it's needed.

I find that this is a nice compromise that gets a lot of the benefits of
static type systems (small, local errors as opposed to behavioural bugs,
ability to express concepts and constraints) without the drawbacks (loss of
interactivity, or inability to escape the type system when it's appropriate).

[1]:
[http://docs.julialang.org/en/latest/manual/types/](http://docs.julialang.org/en/latest/manual/types/)

~~~
lmm
It sounds very ad-hoc? (And Julia's benchmarking has not been reputable in the
past, FWIW).

> without the drawbacks (loss of interactivity, or inability to escape the
> type system when it's appropriate).

One can absolutely have interactivity in a statically typed language, and
virtually all statically typed languages support casting when you absolutely
need to.

~~~
one-more-minute
"Ad-hoc" seems subjective but to me it feels very carefully thought out. And
while I've found the benchmarks to be accurate, I more importantly mean myself
(and many others I know) being easily able to get high performance code when
we need it.

My second paragraph was only a personal take on what works for me, so YMMV,
but nevertheless: I'm not arguing against static type systems in principle,
but these are practical concerns rather than theoretical ones. I don't know of
a mainstream static language that supports a fully dynamic REPL (Haskell's
isn't great) and casting back and forth to `interface{}`, `Object`, or `Any`
everywhere is a huge overhead compared to duck typing. I look forward to a
future language solving those issues, but that's not the case today.

~~~
lmm
> "Ad-hoc" seems subjective but to me it feels very carefully thought out.

I guess I'd like to see a simple formalism.

> myself (and many others I know) being easily able to get high performance
> code when we need it.

That's true for a lot of languages. It doesn't make it Rust-level.

> I'm not arguing against static type systems in principle, but these are
> practical concerns rather than theoretical ones. I don't know of a
> mainstream static language that supports a fully dynamic REPL (Haskell's
> isn't great) and casting back and forth to `interface{}`, `Object`, or `Any`
> everywhere is a huge overhead compared to duck typing. I look forward to a
> future language solving those issues, but that's not the case today.

Shrug, not my experience. I don't know what you mean by "fully generic", but
I've been very happy with the Scala REPL - I've never once thought "this would
be easy in the Python REPL but I can't do it because of the type system".
Casting is indeed a big overhead if you're doing it everywhere, but my
experience is the cases where you need it are extremely rare.

------
zeveb
> The unsatisfying solution used in practice is to give eval() the type Any,
> which is like Object in some OO languages or interface {} in Go: it's the
> type that can have any value. Values of type Any aren't constrained in any
> way, so this effectively removes the type system's ability to help us with
> code involving eval. Languages with both eval and a type system have to
> abandon type safety whenever eval is used.

It's not quite true that they must abandon type safety: rather, they must
abandon a certain kind of compile-time type check. It's not _unsafe_ to use
eval or Any values (i.e., it won't crash the system); one must simply examine
the Any value (at runtime, natch) and do stuff with it.

------
DanWaterworth
> Haskell has no equivalent of the Idris type above, and Go has no equivalent
> of either the Idris type or the Haskell type. As a result, Idris can prevent
> many bugs that Haskell can't, and Haskell can prevent many bugs that Go
> can't. In both cases, we need additional type system features, which make
> the language more complex.

Type checking Idris is much simpler than Haskell.

~~~
tome
> Type checking Idris is much simpler than Haskell.

Is it? How's that?

~~~
DanWaterworth
Haskell's type system has accrued complexity over the years, whereas a small
dependently typed language can be implemented very easily [1].

[1] [https://www.andres-loeh.de/LambdaPi/](https://www.andres-
loeh.de/LambdaPi/)

~~~
AnimalMuppet
That's not a fair comparison, though. A fair comparison might be Haskell's
type system compared to Idris, or a small dependent typed language against a
small type inference language.

~~~
DanWaterworth
The question was, "how's that?". My answer was, it's possible for a small
dependently typed language to be simpler than Haskell.

I'd already made the assertion that Idris was less complicated than Haskell.

------
athenot
I've been very curious about Perl6's gradual typing[1] which, as I understand
it, draws a lot from Haskell's type system but in an optional manner.

    
    
        subset NonNegativeInt of Int where * >= 0;
        
        sub fib(NonNegativeInt $nth) {
          given $nth {
            when 0  { 0 }
            when 1  { 1 }
            default { fib($nth-1) + fib($nth-2) }
          }
        }
    

[1] [http://blogs.perl.org/users/ovid/2015/02/avoid-a-common-
soft...](http://blogs.perl.org/users/ovid/2015/02/avoid-a-common-software-bug-
by-using-perl-6.html)

------
levbrie
Kind of shocked nobody mentioned this, even if it is a bit of an aside, but
umm, I've been dying for anything at all from Gary Bernhardt - I don't even
know what to say except that it makes me hope however unrealistically that
will one day get something like the magnum opus that is "Destroy All Software"
from him again.

~~~
dochtman
You know he just started to post new stuff, right?

[https://www.destroyallsoftware.com/screencasts/catalog](https://www.destroyallsoftware.com/screencasts/catalog)

~~~
levbrie
I know. Mind blown.

------
NhanH
The article is mainly focused on static typing. I guess it makes sense since
the title is "Types". Although reading this, you might just think that dynamic
typing is good for nothing.

The more practical part of the article is great. However, the theoretical part
could use some works. Especially the terminology isn't actually clear.

Takes some example, what is "memory-safe" ? The only example makes it sounds
like bound checking. And what does "no way to escape the language's type
rules" mean? There are some usages of "valid program" and "invalid program"
that makes my spider sense tingling as well.

> However, no dynamic language can match the speed of carefully written static
> code in a language like Rust.

> Any blanket statement of the form "static languages are better at x than
> dynamic languages" is almost certainly nonsense.

Those two quoted statements come from the article.

The section "Arguments for static and dynamic types" didn't mention a single
reason why dynamic typing could be preferable over static-typing. It could at
least have mentioned macro, or hand-waving claim that dynamic typing makes for
prettier code at time (I think the author of python's requests library made
that argument in one of the blog post).

I think the last part should be removed (ranking the language by their typed
system power).

> There's a clear trend toward more powerful systems over time, especially
> when judging by language popularity rather than languages simply existing.

Well, it looks true since we didn't consider any dynamic language at all...

Focusing more on the "Concrete examples of type system power differences"
would have been better for the article, I think. Something along the line of
an example of Non-nullable type with something simple in TS, to optional/maybe
monad, then dependent type that you can implement matrix multiplication (m *
n) with (n * p) and got the type (m * p) back.

~~~
epidemian
> The author is highly probably on the static typing camp.

First and foremost, i don't think dividing people into these clear-cut "camps"
is good for this discussion (and probably for most discussions too...).

Second, i very much doubt that the author is a purist of static typing, or
even a strong proponent of it. He has a series of programming screencasts
called Destroy All Software, and in most the episodes i've seen he uses Ruby,
Bash or Python. So, unless he has dramatically changed his style since then, i
don't think he has any problems using dynamically typed languages.

~~~
NhanH
Sure, I will edit that sentence out.

------
ozy
So what might make dynamic languages easier to write in? I enjoy dynamic
languages more because:

* "double kilometerToMiles(double km) { return km / 1.6 }" here the types are of really low value, yet I have to type them in.

* a mutable list of generic and variable arity event handlers. Really hard to specify as a type (most languages cannot do it). Really easy to use.

* co- and contravariance and lists, the math works exactly opposite of intuition.

* when you change your mind on the types/arity beyond what your IDE can follow, it is a lot of low value work

* if your test suite has 100% coverage of arguments/return-value and field usage, you have type checked your program.

If I am going to write down types, they better be of value to me. Like types
that are null or non-null, or tainted (from the user) or non-tainted. Or two
numbers that are kilometers vs miles. Or only accessible under a certain lock
or other preconditions. And be flow sensitive (kotlin style null checks), not
make me write even more code.

But if you are a dynamic language, do be strong typed (no "1" == 1). Also be
dynamic in arity, that is (part-of) a type. Don't be handwavy scoped, lexical
scoping is probably the only predictable way to do scoping. Fail fast, like
prevent spelling mistakes for non existing field names for example. And allow
this to work: "2 * Vector(10,10)".

And from that perspective there is still quite some room for improvement in
all classes of languages, I suppose, and good that there is lot of movement
and experiments today.

~~~
lucian1900
Writing down the types isn't a necessary property of static type systems. In
Haskell, F# or (OCa)ML, you almost never have to actually mention any types.

I do agree that Java-style types aren't of much value. That catches few errors
at significant cost.

~~~
MrBuddyCasino
Catching logical errors is only one out of several things that static typing
gives you.

Incidentally, right now this is a few positions above this posting:
[https://news.ycombinator.com/item?id=12350063](https://news.ycombinator.com/item?id=12350063)

Better IDE support, a kind of self-documentation and increased performance
come to mind. I know there are counter-examples for each of these points, but
the general trend cannot be denied.

------
lqdc13
I think types can be both frustrating and life-saving.

However, the "danger" for me lies primarily in implicit conversion. That is,
Python vs Lua/JS/R. When writing large programs, some things happen implicitly
and linting may not catch it.

Everything else besides implicit conversion just helps structure the programs
in specific ways and helps speed them up.

------
eximius
So, how do these advanced types work?

For their `add` example, I imagine just having x<y somewhere is sufficient.
But let's say I write a sorting implementation but the compiler can't quite
figure out that it is correct. Is there a provided way to tell the compiler
'trust me, this property is now true'?

Also, I'm assuming the compiler has two classes of errors here: a counter
example vs. unable to prove?

Can anyone point me to documentation on these (in Idris, preferably)?

~~~
icen
In Idris, there are functions `believe_me` and `really_believe_me`, which
assert things to the compiler. They're generally only used for FFI code, or
for extremely tedious proofs.

If you wanted to prove that a list was sorted, you'd write another datatype:

    
    
        IsSorted : Ord a => List a -> Type
    

which represents the proposition that the input list is sorted. You could
encode this by requiring that any new element in the list is either the first
element, or less than or equal to the previous element. Then, your sorting
function could return a dependent pair:

    
    
        (l : List a ** IsSorted l)
    

which can be read as the list, accompanied by a proof that it is, in fact,
sorted. Since your `IsSorted` type requires that the list is sorted, via
construction, you won't be able to supply the output of the function unless
your code is correct (or you use `believe_me`!).

The compiler doesn't generally come up with counterexamples for you; with this
style, if the proof isn't complete, it won't compile.

------
kpmah
I wrote a little article, which has been posted here before, that shows the
power of different type systems and what kind of bugs you can expect to
prevent: [http://kevinmahoney.co.uk/articles/tests-vs-
types/](http://kevinmahoney.co.uk/articles/tests-vs-types/)

------
rpazyaquian
What types are and what they do is all well and good, but what I want to know
is how they would help me write a larger application. Would it help it be more
correct, be written faster, or be more flexible? Does it just help on a line-
by-line programming basis, or can it have any ramifications on a higher level
like product design?

------
amelius
> A type is a collection of possible values

Ok, but do two collections with the same values always correspond to the same
type?

~~~
kd0amg
It depends on the language. If you have, for example, a type-and-effect
system, a type can contain information (like "we wrote to stdout while
computing this") that is never reflected in the value itself. In a formal
setting, it's more appropriate to describe a type as a collection of pieces of
source code, so that `2+3` and `print "computing...\n"; 2+3` have different
types even though they'll evaluate to the same thing.

------
mamcx
I read that type checking is easier with dependent types.

I'm barely aware of adga adn other langs that are used for profs, but wonder
how useful (and what danger ahead) could cause to design a language with this
for more mundane purposes (like, for example, build websites, data
manipulation, gui, etc)

------
th0ma5
Hope this makes it into Wikipedia instead of here.

~~~
paulddraper
It'll get moderated out.

~~~
NhanH
For this article specifically, it shouldn't be on wiki. The quality still
needs a lot of polish, and too much opinions.

~~~
gary_bernhardt
This is a decent summary of why I'm doing it. Wikipedia is full of "high
quality" content, where "high quality" means "directly exposing every detail
that anyone ever thought to nitpick", and this topic is a nitpicker's
paradise. There's value in that detail-heavy style, but it's difficult to
learn from and it's a type of writing that I rarely want to read. As for
opinions (and lack thereof), they prevent Wikipedia from saying many things
that are both well-known and easily stated. The lack of clear summaries of
widespread disagreeing opinions keeps our implicit knowledge implicit.

~~~
NhanH
I agree with your reasoning and your description of Wikipedia.

However, despite all the flaws, wiki is reasonably helpful, and I doubt
filling it with more opinions would be helping the case.

