Not tying the linearity to the type seems quite a bit more confusing and error prone to me.
If a type can be used in different ways, I'd rather have two different types.
The rust way is probably more practically useful from a programmer's perspective, but it isn't as easily understood to be sound from a type theory perspective.
Let's say a function returns a linear value. If I then feed this value into a function that does not specify linearity, all linearity is lost and the function can treat it as a regular non-linear type?
If yes that would seem to require extreme discipline by the programmer to remain useful.
Feel free to give GHC 9.0 a try with a minimal example of your scenario!
withFADR :: (FragileAndDangerousResource #-> a) -> a
addToFADR :: Int -> FragileAndDangerousResource #-> FragileAndDangerousResource
In other words, linear types are purportedly useful precisely because they make it reasonable to ask for that extreme discipline on the programmer's part, because the compiler is right there to tell them when they screw it up--ideally with a helpful error message. withFADR' :: (FragileAndDangerousResource #-> IO ()) -> IO ()
be a better type? My reasoning is if I use the linear identity function lid :: forall a. a #-> a
in the following withFADR lid
I should be able to “break out of” the managed context [1] by the types. Using withFADR'' :: (FragileAndDangerousResource #-> ()) -> ()
is obviously not right since nothing could be done with the `FragileAndDangerousResource`, so I think side effects are necessary.---
[1]: I'm assuming that's what you're going for with `withFADR`.
I admittedly don't really have a great intuition for how to work with this machinery, since I haven't had a chance/excuse to actually play around with it, to be honest; I'm just going off of a SPJ talk on this stuff I watched many months ago, which I mostly understood at the time, and the paper associated with the GHC implementation, which I understood substantially less of.
withFADR :: (FragileAndDangerousResource #-> Unrestricted a) -> Unrestricted a
One can't tag "linear" values in the type system but one can tag non-linear "unrestricted" values. See "borrow" in Section 7.2, page 5:26 of https://arxiv.org/pdf/1710.09756.pdfI'm not expert at this stuff, but when I was interrogating this before, I found the thesis behind https://ncatlab.org/nlab/show/linear-non-linear+logic as a quite old example (1994, only a few years after the first LL paper from Girard in 1987) of separating the linear and non-linear worlds as two categories with an adjudication between them. This is very compatible with linearity in the types.
That all said, I will admit that the "quantitative type theory" line of research gives the current LinearHaskell a big leg up on integrating with dependent types. See https://ncatlab.org/nlab/show/linear-non-linear+logic for the latest plans on that.
I certainly have no retort. My big complaint about the status quo is we can't even have things like Rust's `Box`, but I have no idea what the operational semantics of `f : pi (b : Box<T>) ...` might be, let alone the static semantics!
func :: A -> B
declares a function func that takes one argument of type A and returns a value of type B.I'm guessing a bit, I feel this goes over my head, but here's my layman's interpretation:
In Rust, in order to declare a linear type, you must do so when initializing the variable:
let x = vec![1, 2, 3];
Now x is declared as move-only, so that the vector cannot be duplicated by assigning it to a second variable; after assignment, accessing x will result in a compiler error.In Haskell, you declare a function
func :: A %1 -> B
to indicate that after this function call, accessing A should result in a compiler error. // This moves the argument
fn func(a: A) -> B { .. }
func(x); // x is not accessible after this line
// This borrows the argument, and therefore won't move it
fn func(a: &A) -> B { .. }
func(&x); // x remains accessible after this lineI'm not really sure how to explain it, but basically only functions that are themselves linear can have linear parameters, It's vaguely like "`Fn(A) -> B` only if `A: Copy`", and `FnOnce(A) -> B` only if `A: !Copy`, except there is no `Copy` or `!Copy` and the only say to determine it is `Fn` vs `FnOnce`.