-
Notifications
You must be signed in to change notification settings - Fork 38
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Sources of uniqueness: the daring design debate #130
Comments
The sort of essential characteristic of Now that I think of it, the objection that I wrote just 5min ago above about |
I'm failing to see what the problem is. Is there some boilerplate that you want to reduce? I think I would need an example. |
The “problem” is that there are many contexts in which I could create a unique thing. Say that I have been diligent, and I wrote a mutable array library with both newArray :: Int -> Linear.IO Array
withArray :: Int -> (Array #-> Unrestricted a) #-> Unrestricted a Let's leave aside that it's a bit silly to have to write both these function. (in my proposed scheme they are trivial instances of a more general function. Or such is the hope anyway). But then you come along, and you create a new library with a new monad, let's call it On the other hand, if we had these |
Does defining hm, and then what if we want to create an array where there is not a monad? 🤔 I think I'm starting to understand this issue :) |
That wouldn't be correct anyway, since with this definition |
Is there an abstraction that we can model as a type class here? A type class with meaning (e.g. via laws) is better than convention. |
Maybe? That would actually be a helpful item, since all the convenience function would work for every source bearing type like this. Something like data Source
instance Dupable Source
newSource :: IO Source
withSource :: (Source #-> Unrestricted a) #-> Unrestricted a
-- Not an actual suggestion for names
class SourceBearer a where
grab :: Source #-> a
share :: a #-> (Source, a)
new :: SourceBearer a => IO a
new = grab <$> newSource
with :: SourceBearer a => (a #-> Unrestricted a) #-> Unrestricted a
with scope = withSource (scope . grab)
grabShare :: (SourceBearer a, SourceBearer b) => a #-> (b, a)
grabShare = share . grab Though it's hard to give some real meaning to the type class. Besides it being useful. There probably ought to be an interaction between |
It doesn't look like
|
That's a fair point. So, in order to fill the blanks, we would need an extra type family attached to the type class. And have It's a tad convoluted, though. I'm not sure it's worth the type class. I don't know. |
I had a problem today that I think is relevant to this issue. Most operations on linear mutable vectors rely on the vector being unique, so the usual way to create a vector is to take a linear callback ( Today, I was implementing the However, I had a problem when implementing |
@utdemir this is an interesting enough question that we ought to make a separate issue to discuss it. This comment will act as a link between the two issues. |
While everyone may already realize this, I think it's worth mentioning the prior art in GHC. We have newtype ST s a = ST (State# s -> (# State# s, a #))
IO ~= ST RealWorld -- I have serious qualms about this one. See below. Most unique things can be created within runST :: (forall s. ST s a) -> a The linear type system seems to allow us (but not force us) to drop the quantification, so as I understand it, we could have data FakeWorld
newtype ST a = ST (State# FakeWorld %1-> (# State# FakeWorld, a #))
runST :: ST (Ur a) %1 -> Ur a Now withThingy :: (Thingy %1-> Ur a) %1-> Ur a
withThingy f = runST (mkThingy >>= (pure $!) . f) So as long as we have mkThingy# :: State# s %1-> (# State# s, Thingy #) we can get withAnything :: ST thing %1-> (thing %1-> Ur a) %1-> Ur a
withAnything make f = runST (make >>= (pure $!) . f Can we have source carriers? Well, yeah, a source carrier supports share :: thingy %1-> (# State# s, thingy #) But is that really useful? I'm not totally convinced. The qualms: I think GHC is simply wrong to make |
Question: is the thread-indexed newtype STI s a = STI (State# s %1-> (# State# s, a #))
runST :: (forall s. ST s a) %1-> a That is, the result doesn't need to be unrestricted. |
Ah, I guess class Tok a ~ s => Stately s a where
type Tok a :: Type
share :: a %1-> (# State# s, a #)
realizeWith :: Stately s a => a %1-> (a %1-> STI s b) %1-> b
realizeWith a (STI f)
| (# s, a' #) <- share a
, (# s', b #) <- unSTI (f a') s
, () <- consumeState# s'
= b |
Hrmm.... What I was imagining for multiple state threads won't work quite right. GHC has withForeignPtr :: ForeignPtr a -> (Ptr a -> IO b) -> IO b That's obviously wrong, because withForeignPtr :: ForeignPtr a %1-> (Ptr a -> IO (Ur b)) %1-> IO (Ur b) But this is too rigid— withForeignPtr :: ForeignPtr s a %1-> (forall t. ((IOI s %1-> IOI t) -> Ptr t a -> IOI t b) %1-> IOI s b Suppose something in the inner action needs to create an |
Another option might be to make class Tok a ~ s => Stately s a where
type Tok a :: Type
realizeWith :: a %1-> (a %1-> STI s b) %1-> b This maintains the balance of |
#130 (comment) newKey :: KeyM s (Key s a)
testEquality :: Key s a → Key s b → Maybe (a :∼: b)
runKeyM :: (∀ s. KeyM s a) → a s is always nominal and usually (always?) an infinite kind, really :∼: could be :~~: - this doesn't use typeable at all or fix the kind of the last key parameter. Key is probably an int and KeyM effectively State Int. You can implement ST (very inefficiently) using this. The last parameter in key is usually nominal but in the ST analogue it would be representational (you can coerce STRefs). |
I only just realised that not returning Ur isn't actually sound even if you don't need any clean up, since you can then duplicate the applied scope function e.g. (x = scope id), if x is evaluated once then used multiple times we lose uniqueness. |
I want to share some further development on this issue. In that, I think that I know the solution but it requires GHC support and will take a little time to happen. But it's a convincing enough solution that I do feel comfortable calling it the solution, indeed. This solution is courtesy of the Linear Constraint paper. Without further ado. Let's imagine that we have linear constraints. That is I can write useC :: C %1 => Int
bad :: C %1 => (Int, Int)
bad = (useC, useC) This is what the paper is about (how do you formalise this, and how do you solve for linear constraints). Now, let's imagine that we have a special scoped :: (Linearly %1 => Ur a) %1 -> Ur a
io :: IO (Dict Linearly) Now, this mirrors the That is, instead of newSource $ \token ->
let x = Array.new token 42 in
… I get to write Linearly.scoped $
let x = Array.new 42 in
… It's not super spectacular with just one To complete this story we need one more bit of magic. This one is a little harder, to implement. Specifically, how do we actually support writing two Linearly.scoped $
let x = Array.new 42 in
let y = Array.new 57 in
… This is desugared to: Linearly.scoped $ \token ->
let (token1, token2) = Linearly.dup2 token in
let x = Array.new token1 42 in
let y = Array.new token2 57 in
… (implementing this in the constraint solver is a little tricky, but as a naive strategy to see that it work, we can imagine generating a What do we replace |
EDIT: No, what I wrote here is incomplete. See bottom. The following is already possible today, by using the uncommonly-used {-# LANGUAGE LinearTypes #-}
{-# LANGUAGE ImplicitParams #-}
{-# LANGUAGE RankNTypes #-}
module Linearly (Linearly, linearly) where
-- Linearity token type. Intentionally does not implement `Movable`. Its constructor is private.
data Linearly = Linearly
linearly :: Movable b => ((?lin :: Linearly) => () %1 -> b) %1 -> b
linearly f = let ?lin = Linearly in f () This allows any function which would be unsafe to use in a non-linear context, to restrict itself to contexts in which the Functions which really only are allowed to work in a linear context (AKA functions which are sound only as long as their inputs/output are not aliased) can be constrained to work only under newMArray :: (?lin :: Linearly) => Int %1 -> a -> Array a
fromList :: (?lin :: Linearly) => [a] %1 -> Array a
-- etc. Which then could be used as follows: example list = linearly (\() ->
list
& Array.fromList
& Array.arrOMap (+40)
& Array.toList
) This is not a poor man's implementation of 'linear constraints' as the implicit parameter can be happily used many times. Incidentally that is exactly why it significantly improves on the 'manual linear token producing/consuming' that was discussed in most of this thread. I think this is sound. In other words: EDIT: On closer inspection, since the implicit parameter can be used multiple times GHC does not consider the result of e.g. |
We could just make |
[because alliterative titles are always the best titles]
Edit: I now have a better solution outlined in #130 (comment)
This is something which has been at the back of my mind for a while. And I'm not exactly sure how to approach it.
Here is the problem: when creating some piece of data which needs to be unique (either for resource handling of or mutation or what have you) there are actually many ways to create such a piece of data.
In linear-base, we use two different ones:
withThingy :: (Thingy #-> Unrestricted a) #-> Unrestricted a
newThingy :: M Thingy
run
function of the monad, if theThingy
is to stay uniqueIO
monad, and its cousin theRIO
monad.These are not completely independent ways to create a unique thingy, since
withThingy
can be turned into the monadic style with the continuation monad. But they are not the same either, if only becauseM
need not be a continuation monad.Sometimes one of this styles is required and the other one doesn't really work. But sometimes you may want both. Maybe I can create a linear mutable array with the pure interface, but also with
IO
. Note how neither is an instance of the other. So I'd basically need two distinct creation functions. Meh…But, more importantly, there are other ways to create unique thingy. For instance, if I have any guaranteed unique piece of data, I can create a new linear mutable array, and it will remain unique. And so there starts to be a bit of an
n*m
problem here. Where each type of unique pieces of data need many creation functions. That's unpleasant, to say the least.So I've been thinking that data should be able to carry some kind of token establishing that they are unique. I've been calling them
Source
in my head (we can arrange it so thatSource
is 0-width data, so that they are mostly free).The rest is a bit hazy in my head, still. But I imagine something like the following: we have a number of ways to build uniqueness sources
Moreover sources would be
Dupable
, I assume (which means that they don't guarantee uniqueness, but I think that it is unavoidable).Then we would have
(and maybe
withThingy
andnewThingy
as convenient shorthands for the appropriate composition)To be able to create a thingy out of, say, another thingy, without creating a new scope, we could use the following
Implementing this is what requires
Source
to beDupable
(this, and whatever consume or freeze function that we would have on the type, though these only requireConsumable
).By convention, unique things would always have at least a
fromSource
and aproduceSource
.Something along these lines anyway. Thoughts, further ideas, counterproposals? I want them all!
The text was updated successfully, but these errors were encountered: