
Linear types make performance more predictable - eatonphil
http://blog.tweag.io/posts/2017-03-13-linear-types.html
======
sushisource
As much as I want to use Haskell for real, instead of just reading about and
toying with it, I think the examples in this article are a perfect
illustration of why I find learning Haskell borderline obnoxious at times.

    
    
      data S a
      data R a
      
      newCaps :: (R a ⊸ S a ⊸ IO ()) ⊸ IO ()
      send :: S a ⊸ a ⊸ IO ()
      receive :: R a ⊸ (a ⊸ IO ()) ⊸ IO ()
    
      In this API, S is a send capability: it gives the ability to send a message. R is a receive capability...
    

At least here the author reasonably explains the snippet (this article is
actually just fine generally), but so much of the Haskell examples online are
just like this. Single letters for every type/variable. Why aren't S and R
just called SendCapability and ReceiveCapability? It's almost like the Haskell
community takes pride in being as obscure as possible, which I think is very
possibly true.

~~~
x1798DE
Haskell I've seen often looks a lot like mathematical equations to me, where
the tradition is that the notation is terse, with single letter variables
(possibly with primes or other modifiers) is the norm.

~~~
dpc_pw
Very clever way to make sure the programming language never gains any
significant traction. ;)

~~~
gizmo686
Haskell: avoid success at all costs.

~~~
Magnap
People keep saying this as if it were meant to be parsed "(avoid success) (at
all costs)", but in fact it's supposed to be "avoid (success at all costs)",
in other words "principles > popularity".

~~~
dllthomas
I've always understood it to be, simultaneously, _jokingly_ the former and
seriously the latter.

------
jfoutz
Reminds me of Baez's Rosetta stone paper [1]. It's about some common threads
in physics, math, logic, and programming. A completely different set of
connections that what you'd see in Gödel, Escher, Bach.

I kinda sorta remember linear logic having the option for an infinite source,
and an infinite sink for things you might want to deal with. A business has an
infinite number of customers, but each specific transaction needs to be
handled in a very specific way. You give me a dollar, and i give you a
hamburger or french fries, your choice.

A light, accessible overview of linear logic is here [2]. That has a few nice,
simple examples. It's easier than the wikipedia article.

[1]
[http://math.ucr.edu/home/baez/rosetta.pdf](http://math.ucr.edu/home/baez/rosetta.pdf)

[2]
[http://www.csl.sri.com/~lincoln/papers/sigact92.ps.gz](http://www.csl.sri.com/~lincoln/papers/sigact92.ps.gz)

~~~
tel
Basically, non-linear variables are those with infinite duplication and sinks
(they're "comonoids").

~~~
mafribe
Another option is affine types that give you "at most once" semantics, in
contrast to linear types' "exactly once". If your language has non-termination
then affine is easier to enforce. (Cf Rust.)

~~~
tupshin
Sure, but rust benefits from must_use annotations, which effectively turn
those annotated affine types into linear types. Turns out they are both useful
in a very overlapping venn-diagram kind of way.

~~~
mafribe
How does this deal with non-termination? Eg.

    
    
       declare x (must_use);
       P;
       use(x)
    

(I apologies for the non-Rust-conforming pseudo-code.) The type-checker won't
be able to tell if P terminates or not, whence x's use can't be enforced.
That's a direct consequence of Rice's theorem (which in turn follows directly
form the non-computability of the halting predicate).

------
harpocrates
This is a big deal. Right now, Haskell (like most GC-ed languages) has one big
heap that gets GC-ed regularly. The _only_ time memory is reclaimed is when
the GC runs.

By retrofitting Haskell with linear types, there would be another linear heap.
In that heap, there is no GC - resources get freed as soon as the linearly
typed value is used. On can imagine programs properly annotated with linear
types that don't even need ANY GC.

This is an even bigger deal in Haskell than in most other languages because
Haskell, being immutable, performs a ton of allocations. Want to update a
field? Nope, but you can allocate the whole object again with just that field
changed. If most of that work could be moved to a linear heap which doesn't
need GC... well you get the point.

~~~
theseoafs
I have not seen any evidence that GC is the primary bottleneck in Haskell, or
that a form of compiler-driven manual memory management would be faster.

~~~
tupshin
GC has non-zero overhead compared with linearly typed de-allocations, but
that's not the main point. The title mentions predictability, and one of the
big blockers to using almost any GC-ed language is the lack of predictability
about runtime latency, which is very different than throughput, which I
generally associate with the term bottleneck.

Additionally, predictable performance is about what the programmer can predict
(whether you are talking about throughput or latency), and the argument is
that linear types make it much easier to reason about the performance of a
system, hence making it more predictable.

~~~
jholman
IANA expert, but as far as I can tell...

1) When it comes to sources of unpredictability, laziness is a way bigger
problem than garbage collection

2) Assuming that by "GC" we mean "some variation on mark-and-sweep" (rather
than meaning "any type of automatic memory management", which I hope is not
the usage of anyone in this conversation), GC cost (in terms of CPU-time
consumed) doesn't care how many allocations you've done, nor how much garbage
it needs to collect. All it cares about is the size of the working set at the
time that it runs. As such, all those lingering copies of the old tree nodes
(etc, etc) are irrelevant. So I don't see why Haskell's additional allocations
should cause GC to be any more of an overhead.

~~~
dllthomas
> So I don't see why Haskell's additional allocations should cause GC to be
> any more of an overhead.

In this kind of garbage collector, time of an individual garbage collection
should not depend on the amount of garbage that has been generated. However,
producing more garbage should mean more frequent collections, and so more time
spent in GC overall.

------
empath75
Linear logic is also useful for describing the rules of games. You can set up
the games starting state as resources and the games rules are axioms and
winning the game is equivalent to proving a certain outcome.

This was also super interesting:
[https://www.cs.cmu.edu/~cmartens/lpnmr13-short.pdf](https://www.cs.cmu.edu/~cmartens/lpnmr13-short.pdf)

------
brudgers
Related paper: [https://www.microsoft.com/en-us/research/wp-
content/uploads/...](https://www.microsoft.com/en-us/research/wp-
content/uploads/2017/03/haskell-linear-submitted.pdf)

------
BuuQu9hu
It's interesting that they use the word "capability" here. These kinds of
single-shot values show up in the capability-theory literature occasionally. I
suspect that linear types might be equivalent in structural power to the
typical notion of "perfect encapsulation" in object-capability security, but I
don't yet have a proof.

------
fmap
This is wonderful, can't wait to use it for my own code. :)

Though at the moment the semantics isn't entirely clear to me. For instance,
in the protocol example, shouldn't the pair be using multiplicative linear
conjunction? It doesn't matter so much for a function receiving two Ints, but
what's the semantics for a function receiving multiple ressources?

I guess you can use an impredicative encoding, e.g.

    
    
      A ⊗ B = forall C. (A ⊸ B ⊸ C) ⊸ C
    

but isn't it more usable if it's built into the compiler?

------
Ericson2314
Oh but I want a linear kind though! I'd love to reimplement Rust with GHC for
some kick-ass safe interopt.

~~~
nickpsecurity
Did you actually mean Rust done in Haskell or Haskell done in Rust for Rust
stdlib?

~~~
jes5199
Implement Haskell in Rust and then implement Rust on top of that

~~~
nickpsecurity
I think that might be a waste since we have better tech than Rust for
compilers. Better of doing it in COGENT and Haskell simultaneously. Use tools
such as QuickCheck on Haskell part. Production code will be COGENT that's
certified to machine code. Once that's finished, use the compiler to compile
itself with RTS left in COGENT.

[https://ts.data61.csiro.au/projects/TS/cogent.pml](https://ts.data61.csiro.au/projects/TS/cogent.pml)

~~~
kamatsu
Hi, I'm the creator of Cogent.

I don't think Cogent would be much good for writing a compiler, actually (it
doesn't natively support recursive data structures like ASTs). It's designed
for OS components like file systems.

~~~
nickpsecurity
Wow, what a small world. :) I evangelize high-assurance tech and R&D on forums
like this. I wasn't aware of this limitation of COGENT. I maybe wrongly
assumed one could write a simple, non-optimizing compiler if people had
written two filesystems in it. The latter seemed more complex. I'll hold off
on that recommendation then.

I did want to learn some more about the strengths and limitations of your tool
to generate ideas for other researchers. Send an email to the address in my
profile. I'll reply in a few days with some questions that people here might
have.

------
tromp
I wonder how we are supposed to type ⊸ in ye olde ASCII?

~~~
harpocrates
IIRC There is some discussion of using "-o", but then there are a whole slew
of lexing/parsing issues that emerge since "o" is not part of the (vast!)
class of characters Haskell considers as valid for use in operators.
Regardless of that, I do kind of hope "-o" gets adopted...

~~~
lepton
I think "\--[star]" could do well: the "[star]" emphasizes the "pop" in the
lolly. Touch it once, and it's gone.

EDIT: Having trouble typesetting the asterisk!

~~~
Jtsummers
* gets swallowed up for italicizing text on HN. Best way to get literal text is a line with two (or more) leading space characters:
    
    
      --*

------
mamcx
I still not get what is the deal here. So I see the types, but not why making
them linear is good.

~~~
yawaramin
Linear types guarantee at compile time that you can use each value only and
exactly once--so no use after free errors, no memory leaks.

~~~
mamcx
Ok, and know that is not possible with normal types and follow the flow?

~~~
tel
No, as it turns out in presence of recursion it's undecidable to know that in
general. Inference in a linear logic system can answer when it's possible some
of the time. Programmer-written annotations can assert it's the case. Type-
checkers can ensure that assertions actually hold.

------
rurban
How on earth do those functional programmer folks always come up with the
worst names for simple existing concepts, with totally different names? Worse
than perl.

-> instead of comma?

-o / linear instead of refcaps?

"linear types" are certainly not linear. They are a simple reference
capability, already in usage for decades.
[https://en.wikipedia.org/wiki/Object-
capability_model](https://en.wikipedia.org/wiki/Object-capability_model)

e.g. [https://tutorial.ponylang.org/capabilities/reference-
capabil...](https://tutorial.ponylang.org/capabilities/reference-
capabilities.html)

Consuming a value changes its type to enable reasoning for concurrency.

As to the claim: Sure. Not only makes a refcaps type system concurrency
predictable, it also makes it decidable at compile-time, and much faster. rust
could learn a lot from this to avoid locking.

~~~
irishsultan
> -> instead of comma?

Because a comma would be a very unusual notation to separate the parameter and
return value of a function.

For example a function with type "a -> b -> c" is not a function taking three
parameters, it's a function taking something of type a returning another
function of type (b -> c). Writing that as "a, b, c" would be an extremely
weird way to write that.

~~~
rurban
Yes, I was wrong. I should have said () vs ->. You would write your "a -> b ->
c" as

    
    
      (a) -> b
      (b) -> c
    

and then combined: ((a) -> b) -> c. A function taking a function. But Haskell
just flattens nested these lists.

E.g. take this:

    
    
      inc :: Num a => a -> a
    

is normally written as

    
    
      func inc (Num a) -> Num;
    

A function taking a Num and returning a Num. The a is normally written as T, a
generic type variable, thus forbidding type-promotions from e.g. overflowing
Integer to Num. This inc will wrap around.

This is normally written without dependent types as generic for all subtypes.

    
    
      func inc (Num a) -> Num;
      func inc (Integer a) -> Integer;
    

Now Haskell calls that "pattern matching", but of course it is not pattern
matching, it is polymorphism, static dispatch based on signatures. Pattern
matching has a well-established meaning already, if strings or structural
matching. Dispatch does much more than just matching. A match only finds exact
candidates, dispatch also finds the closest candidate, which doesn't need to
match.

~~~
irishsultan
Haskell doesn't call that pattern matching, or at least I've never heard that
usage.

What Haskell calls pattern matching is if you have a single type (e.g. Maybe
a) with two different constructors (in this case Just and Nothing) where "Just
something" will match with a value constructed with that first constructor and
Nothing with the second constructor.

Note that his isn't the only thing you have wrong, there are no values of type
Num, because Num is not a type. In the expression "inc :: Num a => a -> a" the
only type is a, which has a restriction namely that the type has to implement
the Num typeclass. The parameters don't have a name in the type signature.

