A good practical description of linear logic (but not types) is here: https://hashingit.com/elements/research-resources/1994-03-Fo...
Yes it's about how Forth implements linear logic without realizing it ;).
My understanding of "Linear Types" is that they are geared towards declaring values/types that can be used only once.
If this sounds weird, I thought so too. Except it turns out there a number of (fairly common) scenarios in which you might want to ensure that a value is only possible to be used once.
I've drank too much so I can't conjure many to mind, but what I can think of is something like a buffer in a lexer, where you want to ensure that a token is consumed once and then not available.
There are some decent examples in the Tweag repo for the GHC Linear Types proposal:
The OP has some examples. One of them is creating verified state machines (i.e. stating invariants that different states and transitions should obey all the way up to entirely specifying the abstract state machine). The paper uses linear types to replace Idris 1's ST type. Another is enforcing order of messages to be sent in a bidirectional communication protocol. There's also a lot more low-level examples (make sure that files get closed after use, make sure mutable variables are used correctly, etc.).
In general invariants of the form "verify that this happens after that happens" (and note that this phrase has two meanings which both are covered, verify that e.g. a clean up action always occurs when an action that requires clean up has happened and also verify that e.g. an action occurs after another action, and not before) can be enforced by linear types, which covers a large amount of what happens with stateful computing.
It would actually solve a lot of issues in Rust, but unfortunately would be an insane amount of work to introduce because they would invade a lot of the current type system
Linear types lets you do things like "this data can only be written to a file once".
It gives the example of a key in a database, which can return multiple types of values:
type DB = (k: Key) => Option[k.Value]
Where the type of "Key" is: trait Key { type Value }
You can declare generic values of type "Key[Value]" and attempting to look them up in the DB can result in more than one kind of type, dependent on the value being looked up.https://docs.scala-lang.org/scala3/book/types-dependent-func...
interface Key<V> { ... }
class Name extends Key<String> { ... }
class Age extends Key<Integer> { ... }
interface DB {
public <V> Optional<V> get(Key<V> k) { ... }
} Name nameKey = ...;
Age ageKey = ...;
Optional<String> name = db.get(nameKey);
Optional<Integer> age = db.get(ageKey);Optional<1> and Optional<“Name”>. You can only have types as arguments.
C++ has some dependent types in it allows you to have generic arguments be ints std::array<…, 4> but it still does not allow most values as generic type argument.
Here's an example that extends it slightly and can't be emulated by Java.
// I forget if I need to curry the second argument in a separate argument list
// I'm not next to a computer with scalac at the moment, so I'll just use separate argument lists
type DB = (k: Key) => k.IsVerified => Option[k.Value]
trait Key {
type Value
val keyValue: Value
case class IsVerified(underlyingBool: Boolean)
}
val myDB: DB =
key => isVerified => x match
case IsVerified(bool) =>
if bool then
// return the actual value
doSomething()
else
None
def verifyKey(key: Key): key.IsVerified =
if isValidKey(key) then
key.IsVerified(true)
else
key.IsVerified(false)
val key0: Key = ...
val key1: Key = ...
val key0Verification: key0.IsVerified = verifyKey(key0)
// Fails to compile
// You're trying to cheat!
// You only verified key0 and are trying to use its verification to bypass
// verifying key1
myDB(key1)(key0Verification)In theory you can do this kind of thing in Java, but you have to give each key its own type (cumbersome because Java doesn't have singleton types) and carry the type parameters arbitrarily far back through the call graph. E.g. imagine writing a function that appends 3 type-length vectors together - in a dependently-typed language you write this as:
def append[T, M, N, L](first: Vec[T, M], second: Vec[T, N], third: Vec[T, L]): Vec[T, M + N + L] = ...
In Java you'd have to write this as: <T, M, N, L, P, R> Vec<T, P> append (first: Vec<T, M>, second: Vec<T, N>, third: Vec<T, L>, isSum1: IsSum<M, N, P>, isSum2: IsSum<P, L, R>) { ... }
And as you write more and more code you have exponentially more type parameters, because you have no way to just evaluate functions of type parameters, so you have to pass markers that represent all of your type relationships right from the initial input part of your program all the way down.This isn't really the thing that dependent types provide though. If you have type-level computation without dependent types that's enough to avoid this problem (and e.g. Scala has this). However, dependent types are when the `+` in your first append is the same `+` as the usual runtime `+` rather than a separate type-level function that only works with types, not runtime values. Scala does not have this. Its dependent types are much more limited.
app : Vect n a -> Vect m a -> Vect (n + m) a
where we're using the value-level addition operator to express the type of the result; specifically, that appending a Vect of length m to a Vect of length n will give a Vect of length (n + m). [1][1] Taken from https://www.idris-lang.org/pages/example.html
Scala has only a very limited version of dependent types. Even its "dependent function types" are a more restricted version of general dependent function types. Full dependent types can in a certain sense be even simpler than non-dependently-typed languages (e.g. you no longer need a separate notion of generics).
I don't think that's a particularly enlightening example, mostly because we can implement such a function without any linear or dependent types.
Consider that a list (of some element type T) could contain zero Ts, or one T, or two Ts, etc. i.e.
List<T> = 1 + T + (T × T) + (T × T × T) + ...
Where:- `1` is a type containing only one value, like 'Unit', 'Null', 'Nil, etc. We use this to represent the empty list.
- 'X + Y' is the (tagged) union of types X and Y; AKA a sum type, or an Either
- 'X × Y' is the type of tuples containing an X and a Y; AKA a product type, or a Pair
Non-empty lists are almost the same, except we don't want to allow the empty list. Hence we need to get rid of the '1' type, to get something like this:
NonEmptyList<T> = T + (T × T) + (T × T × T) + ...
Just using the normal rules of arithmetic, we can see that NonEmptyList<T> is the same as List<T>, except everything has been multiplied by T: NonEmptyList<T> = T + (T × T) + (T × T × T) + (T × T × T × T) + ...
= (T × 1) + (T × T) + (T × T × T) + (T × T × T × T) + ...
= T × ( 1 + T + ( T × T) + ( T × T × T) + ...)
= T × List<T>
Hence we can implement NonEmptyList<T> as the type 'T × List<T>', i.e. tuples containing a T and a List<T>. Intuitively, those are the "head" and "tail" of the non-empty list; or equivalently, Cons constructs a tuple, and non-empty lists are those whose outermost constructor is always Cons.Note that we can go the other way too, if our language provided non-empty lists and we want to allow empty ones too:
List<T> = 1 + T + (T × T) + (T × T × T) + ...
= 1 + NonEmptyList<T>
This time, we a value of type List<T> is either a unit value (representing the empty list), or a non-empty list. This pattern of "adding one" to a type is usually called 'Maybe' or 'Option': Maybe<T> = 1 + T
Hence simplifying the above to: List<T> = Maybe<NonEmptyList<T>>