RFC: Top level mutable state in terms of pure-function initialization?

A pure function (enumToIORef :: (Enum a, Defaultable b) => a -> IORef b). This function returns referentially transparently an IORef as a function of its "seed" with the guarantee that the IORef returned is identical if and only if the seed is. This function can be used to implement top level mutable state in a module. The module can specifically create an enumerated type for this and not export it, thereby removing any possibility of another module passing the seed type and conflicting. The Defaultable class is added in this case to implement only a single method (defaultValue :: Defaultable a => a -> a). This is conceptionally the 'simplest' value of the type, such as the empty list, the number 0, False, the null character &c. The IORef returned by enumToIORef would be initialized before being written to to this specific default value of its type. This approach is chosen because it is impossible to initialize it to user specified value because enumToIORef can be called twice with the same seed but a different initial value. In the alternative it is also possible to do without the default value and say the IORef returned is the same if and only if the seed and the initial value given are the same. Allowing the function to remain referentially transparent as well. This would probably require for good semantics the underlying type of the IORef to be a member of EQ...? All this would of course require that newIORef and enumToIORef never produce the same IORef. Aside its limitations of the type IORef's initialized with this method can carry, I do believe they cover the vast majority of use cases of top level mutable state? Caveats?

This has the same issue as just using a top-level (IORef b). Assume two use cases: let x = enumToIORef 5 in (x, x) (enumToIORef 5, enumToIORef 5) It's "obvious" in the first case that you have one ref that is used twice, while it is "obvious" in the second case that you have two refs containing the same value. But this breaks the rule that (let x = y in f x) = (f y), because the left is a single ref while the right is two refs. There's likely other problems with it too, but this is what stands out to me. On 12/15/2013 6:39 AM, EatsKittens wrote:
A pure function (enumToIORef :: (Enum a, Defaultable b) => a -> IORef b). This function returns referentially transparently an IORef as a function of its "seed" with the guarantee that the IORef returned is identical if and only if the seed is. This function can be used to implement top level mutable state in a module. The module can specifically create an enumerated type for this and not export it, thereby removing any possibility of another module passing the seed type and conflicting.
The Defaultable class is added in this case to implement only a single method (defaultValue :: Defaultable a => a -> a). This is conceptionally the 'simplest' value of the type, such as the empty list, the number 0, False, the null character &c. The IORef returned by enumToIORef would be initialized before being written to to this specific default value of its type. This approach is chosen because it is impossible to initialize it to user specified value because enumToIORef can be called twice with the same seed but a different initial value.
In the alternative it is also possible to do without the default value and say the IORef returned is the same if and only if the seed and the initial value given are the same. Allowing the function to remain referentially transparent as well. This would probably require for good semantics the underlying type of the IORef to be a member of EQ...?
All this would of course require that newIORef and enumToIORef never produce the same IORef.
Aside its limitations of the type IORef's initialized with this method can carry, I do believe they cover the vast majority of use cases of top level mutable state?
Caveats?
_______________________________________________ Haskell-Cafe mailing list Haskell-Cafe@haskell.org http://www.haskell.org/mailman/listinfo/haskell-cafe

I don't think you understood me, maybe I wasn't clear enough but what I meant was that the ref was identical if and only if the enum seed is identical. (enumToIORef 5, enumToIORef 5) consists of two identical references. The enumToIORef function is completely reverentially transparent and its order of execution should not matter. (enumToIORef 5) :: Defaultable a => IORef a should always return the same ref. It will return a different ref however for each type a. In this sense, the hypothetical program: x = enumToIORef 0; y = enumToIORef 0; main = do { writeIORef x "Hello, World"; -- writes to ref x message <- readIORef y; -- reads from ref y, which is identical putStrLn message; }; Should output "Hello, World\n" However, as noted by Roman, a subtle bug occurs in this case: x = enumToIORef 0; y = enumToIORef 0; main = do { writeIORef x "Hello, World"; -- writes to ref x message <- readIORef y; -- reads from ref y, which is NOT identical print (message + 1); }; In this case the compiler deduces (with monomorphic restriction) that (y :: IORef Int) while (x :: IORef String), as such since enumToIORef is asked to return two different types each time it returns two different references. message is in this case read from the default value of 0 and "1\n" is output. On Sunday, 15 December 2013 14:00:17 UTC+1, Joe Quinn wrote:
This has the same issue as just using a top-level (IORef b). Assume two use cases:
let x = enumToIORef 5 in (x, x) (enumToIORef 5, enumToIORef 5)
It's "obvious" in the first case that you have one ref that is used twice, while it is "obvious" in the second case that you have two refs containing the same value. But this breaks the rule that (let x = y in f x) = (f y), because the left is a single ref while the right is two refs.
There's likely other problems with it too, but this is what stands out to me.
On 12/15/2013 6:39 AM, EatsKittens wrote:
A pure function (enumToIORef :: (Enum a, Defaultable b) => a -> IORef b). This function returns referentially transparently an IORef as a function of its "seed" with the guarantee that the IORef returned is identical if and only if the seed is. This function can be used to implement top level mutable state in a module. The module can specifically create an enumerated type for this and not export it, thereby removing any possibility of another module passing the seed type and conflicting.
The Defaultable class is added in this case to implement only a single method (defaultValue :: Defaultable a => a -> a). This is conceptionally the 'simplest' value of the type, such as the empty list, the number 0, False, the null character &c. The IORef returned by enumToIORef would be initialized before being written to to this specific default value of its type. This approach is chosen because it is impossible to initialize it to user specified value because enumToIORef can be called twice with the same seed but a different initial value.
In the alternative it is also possible to do without the default value and say the IORef returned is the same if and only if the seed and the initial value given are the same. Allowing the function to remain referentially transparent as well. This would probably require for good semantics the underlying type of the IORef to be a member of EQ...?
All this would of course require that newIORef and enumToIORef never produce the same IORef.
Aside its limitations of the type IORef's initialized with this method can carry, I do believe they cover the vast majority of use cases of top level mutable state?
Caveats?
_______________________________________________ Haskell-Cafe mailing listHaskel...@haskell.org javascript:http://www.haskell.org/mailman/listinfo/haskell-cafe

1. Well, that creates a loophole in the type system:
let
x :: IORef Int
x = enumToIORef 0
y :: IORef Bool
y = enumToIORef 0
in writeIORef x (5 :: Int) >> (readIORef y :: IO Bool)
2. There's also a less obvious loophole with your Defaultable version:
let
x :: Defaultable a => IORef a
x = enumToIORef 0
in writeIORef x (5 :: Int) >> (readIORef x :: IO Bool)
3. In your default-less version there's a problem of doing this:
x = enumToIORef 0 True
y = enumToIORef 0 False
Now, x and y presumably refer to the same IORef, but the initial
contents of that IORef will depend on whether you access it through x or
y.
4. Finally, the implementation of enumToIORef can only inspect it by
converting to Int, and cannot distinguish different enums that
correspond to the same number.
This last problem can be solved by adding a Typeable constraint,
because TypeReps carry the information about where (module, package) the
type was defined.
Roman
* EatsKittens
A pure function (enumToIORef :: (Enum a, Defaultable b) => a -> IORef b). This function returns referentially transparently an IORef as a function of its "seed" with the guarantee that the IORef returned is identical if and only if the seed is. This function can be used to implement top level mutable state in a module. The module can specifically create an enumerated type for this and not export it, thereby removing any possibility of another module passing the seed type and conflicting.
The Defaultable class is added in this case to implement only a single method (defaultValue :: Defaultable a => a -> a). This is conceptionally the 'simplest' value of the type, such as the empty list, the number 0, False, the null character &c. The IORef returned by enumToIORef would be initialized before being written to to this specific default value of its type. This approach is chosen because it is impossible to initialize it to user specified value because enumToIORef can be called twice with the same seed but a different initial value.
In the alternative it is also possible to do without the default value and say the IORef returned is the same if and only if the seed and the initial value given are the same. Allowing the function to remain referentially transparent as well. This would probably require for good semantics the underlying type of the IORef to be a member of EQ...?
All this would of course require that newIORef and enumToIORef never produce the same IORef.
Aside its limitations of the type IORef's initialized with this method can carry, I do believe they cover the vast majority of use cases of top level mutable state?
Caveats?
_______________________________________________ Haskell-Cafe mailing list Haskell-Cafe@haskell.org http://www.haskell.org/mailman/listinfo/haskell-cafe

1. Well, since x and y are not of the same type they would in this case
contain a different IORef. The read in this case would read False since y
is initialized to false as the default of Bool.
2. In this example it would again be two different IO refs since the type
is different. It would again on demand create a different IORef for Int and
Bool.
3. No, they refer to a different one as I said, the default-less version
creates the IORef to be unique if and only if both the seed and
initialization are the same.
4. Agreed, I overlooked that not all enum instances are typable. (I believe
they also need to be an instance of EQ?)
On 15 December 2013 14:38, Roman Cheplyaka
1. Well, that creates a loophole in the type system:
let x :: IORef Int x = enumToIORef 0
y :: IORef Bool y = enumToIORef 0 in writeIORef x (5 :: Int) >> (readIORef y :: IO Bool)
2. There's also a less obvious loophole with your Defaultable version:
let x :: Defaultable a => IORef a x = enumToIORef 0 in writeIORef x (5 :: Int) >> (readIORef x :: IO Bool)
3. In your default-less version there's a problem of doing this:
x = enumToIORef 0 True y = enumToIORef 0 False
Now, x and y presumably refer to the same IORef, but the initial contents of that IORef will depend on whether you access it through x or y.
4. Finally, the implementation of enumToIORef can only inspect it by converting to Int, and cannot distinguish different enums that correspond to the same number.
This last problem can be solved by adding a Typeable constraint, because TypeReps carry the information about where (module, package) the type was defined.
Roman
* EatsKittens
[2013-12-15 12:39:20+0100] A pure function (enumToIORef :: (Enum a, Defaultable b) => a -> IORef b). This function returns referentially transparently an IORef as a function of its "seed" with the guarantee that the IORef returned is identical if and only if the seed is. This function can be used to implement top level mutable state in a module. The module can specifically create an enumerated type for this and not export it, thereby removing any possibility of another module passing the seed type and conflicting.
The Defaultable class is added in this case to implement only a single method (defaultValue :: Defaultable a => a -> a). This is conceptionally the 'simplest' value of the type, such as the empty list, the number 0, False, the null character &c. The IORef returned by enumToIORef would be initialized before being written to to this specific default value of its type. This approach is chosen because it is impossible to initialize it to user specified value because enumToIORef can be called twice with the same seed but a different initial value.
In the alternative it is also possible to do without the default value and say the IORef returned is the same if and only if the seed and the initial value given are the same. Allowing the function to remain referentially transparent as well. This would probably require for good semantics the underlying type of the IORef to be a member of EQ...?
All this would of course require that newIORef and enumToIORef never produce the same IORef.
Aside its limitations of the type IORef's initialized with this method can carry, I do believe they cover the vast majority of use cases of top level mutable state?
Caveats?
_______________________________________________ Haskell-Cafe mailing list Haskell-Cafe@haskell.org http://www.haskell.org/mailman/listinfo/haskell-cafe

Ok, it could work, but it can lead to very subtle bugs. You change the
type of IORef during refactoring (and forget to change it in a different
place), and your program now works wrong (but silently so), because now
it's creating two IORefs instead of one.
A better way would be to have phantomly-typed identifiers, like
class IsRefId c where
initialContents :: c a -> a
toInt :: c a -> Int
data MyRefId a where
Ref1 :: RefId Int
Ref2 :: RefId Bool
...
instance IsRefId MyRefId where ...
idToIORef :: IsRefId c => c a -> IORef a
Still can be abused (by not constraining phantom types properly), but
can also provide safety if used right.
(And no, Eq is not strictly necessary, because you convert to integers
anyway.)
Roman
* EatsKittens
1. Well, since x and y are not of the same type they would in this case contain a different IORef. The read in this case would read False since y is initialized to false as the default of Bool.
2. In this example it would again be two different IO refs since the type is different. It would again on demand create a different IORef for Int and Bool.
3. No, they refer to a different one as I said, the default-less version creates the IORef to be unique if and only if both the seed and initialization are the same.
4. Agreed, I overlooked that not all enum instances are typable. (I believe they also need to be an instance of EQ?)
On 15 December 2013 14:38, Roman Cheplyaka
wrote: 1. Well, that creates a loophole in the type system:
let x :: IORef Int x = enumToIORef 0
y :: IORef Bool y = enumToIORef 0 in writeIORef x (5 :: Int) >> (readIORef y :: IO Bool)
2. There's also a less obvious loophole with your Defaultable version:
let x :: Defaultable a => IORef a x = enumToIORef 0 in writeIORef x (5 :: Int) >> (readIORef x :: IO Bool)
3. In your default-less version there's a problem of doing this:
x = enumToIORef 0 True y = enumToIORef 0 False
Now, x and y presumably refer to the same IORef, but the initial contents of that IORef will depend on whether you access it through x or y.
4. Finally, the implementation of enumToIORef can only inspect it by converting to Int, and cannot distinguish different enums that correspond to the same number.
This last problem can be solved by adding a Typeable constraint, because TypeReps carry the information about where (module, package) the type was defined.
Roman
* EatsKittens
[2013-12-15 12:39:20+0100] A pure function (enumToIORef :: (Enum a, Defaultable b) => a -> IORef b). This function returns referentially transparently an IORef as a function of its "seed" with the guarantee that the IORef returned is identical if and only if the seed is. This function can be used to implement top level mutable state in a module. The module can specifically create an enumerated type for this and not export it, thereby removing any possibility of another module passing the seed type and conflicting.
The Defaultable class is added in this case to implement only a single method (defaultValue :: Defaultable a => a -> a). This is conceptionally the 'simplest' value of the type, such as the empty list, the number 0, False, the null character &c. The IORef returned by enumToIORef would be initialized before being written to to this specific default value of its type. This approach is chosen because it is impossible to initialize it to user specified value because enumToIORef can be called twice with the same seed but a different initial value.
In the alternative it is also possible to do without the default value and say the IORef returned is the same if and only if the seed and the initial value given are the same. Allowing the function to remain referentially transparent as well. This would probably require for good semantics the underlying type of the IORef to be a member of EQ...?
All this would of course require that newIORef and enumToIORef never produce the same IORef.
Aside its limitations of the type IORef's initialized with this method can carry, I do believe they cover the vast majority of use cases of top level mutable state?
Caveats?
_______________________________________________ Haskell-Cafe mailing list Haskell-Cafe@haskell.org http://www.haskell.org/mailman/listinfo/haskell-cafe
participants (3)
-
EatsKittens
-
Joe Quinn
-
Roman Cheplyaka