I propose adding the following two functions to the refs
and st
packages:
new' :: forall s. s -> Ref s
new' :: forall a r. a -> STRef r a
And then rewrite the current new
as new a = pure $ new' a
.
The proposal comes from extensive work with purescript-event
and finding myself often needing to create an event with a push function in a non-effectful context. The signature for create is currently create :: forall a. Effect (EventIO a)
, but the Effect
just exists so that Ref.new
can be called: otherwise, it could be written create :: forall a. Unit -> EventIO a
.
The effectful nature of new
creates a weird situation in the internals of the event
library, where unsafePerformEvent
is called in a pure function just to make an event. This usage of unsafePerformEvent
winds up being is necessary in other situations as well when working with purescript-events
. If new
were a pure function, on the other hand, event creation could be pure as well.
So that’s the upside of the proposed new'
. The downside is that it gets into murky waters of what a pure function is. If the definition of a pure function is that it must always return the same output for a given input, you can argue that new' :: a -> Ref a
is pure because the two references, at their moment of creation, are equal. At the same time, it doesn’t make sense for Ref a
to have an Eq
instance, so even though the function is conceptually pure, it is not demonstrably pure.
Back to the upside, this change would get rid of several unsafePerformEffect
-s spread over various libraries and web apps.
Thoughts?
Maybe this is nitpicky but IMHO for something to be considered pure it needs to be referential transparent - meaning you can replace the assigned name with the expression without changing the meaning of the code.
In this sense I think new'
cannot be pure as you cannot replace the assigned name with the call to new'
without changing the program.
I understand the need but I’d prefer to keep it pure.
2 Likes
That’s an interesting take on it - I still need to fully process it, but there’s an argument that the condition “you can replace the assigned name with the expression without changing the meaning of the code” holds here.
Example 1
a :: Int
a =
let
b = new' 0
c = b
in
42
b :: Int
b =
let
b = new' 0
c = new' 0
in
42
In Example 1, both a
and b
yield 42, even though one replaces the assigned name with the expression and the other doesn’t.
Example 2
a :: Effect Int
a = do
let
b = new' 0
c = b
write c 42
read b
b :: Int
b = do
let
b = new' 0
c = new' 0
write c 42
read b
In example 2, the programs are different. However, we are in the Effect
context in example 2, where there is no contract that two executions of the same program will yield the sam result.
**
It could be, however, that I don’t fully understand the meaning of Effect
. My intuitive understanding comes from Learn You a Haskell where the author contrasts pure functions to the IO
monad:
While this may seem a bit limiting when you’re coming from an imperative world, we’ve seen that it’s actually really cool. In an imperative language, you have no guarantee that a simple function that should just crunch some numbers won’t burn down your house, kidnap your dog and scratch your car with a potato while crunching those numbers.
When we are in Effect
, we are in the land of kidnapped dogs and scratched cars, so all bets are off, whereas in the pure function from Example A it’s impossible to change the control flow of the program.
I guess it depends on your take on Effect/IO - I think there is a way to interpret Effect-values in a way that the value/expression itself is pure - side-effect only enter when the runtime evaluates main
- at least that’s my understanding on Haskell-IO
and why Haskell is still considered a pure language.
But you are right - this case is a bit mind-boggling and confusing.
But if you have new
as an Effect
then I think you can still argue that new 42
is referential transparent as this is not the reference but an Effect
generating a new reference and it’s indeed irrelevant if I write
a :: Effect Int
a = do
let new42Ref = new 42
r1 <- new42Ref
r2 <- new42Ref
...
or
a :: Effect Int
a = do
r1 <- new 42
r2 <- new 42
...
Don’t know if this is helpful but can you see the point?
I think/feel that’s it is clear that new
indeed involves some side-effects (create a new/fresh initialized reference) and that it’s type should reflect that.
3 Likes
That’s a good point, it’s a good argument for disallowing the pure version.
Newb here
This is interesting.
I’ve generally thought of Ref as exposing underlying implementation details that you otherwise can safely abstract away in a pure language. There’s some extra bit of information (an opaque token of some sort) that you put alongside the value a Ref holds that lets you use the Ref, right?
new' :: a -> Ref a
is pure because the two references, at their moment of creation, are equal
My gut-check here goes the opposite way. If a
is the same, the returned Ref a
still needs to somehow generate that token (for example, a memory location I suppose). In the pure new'
that token must also be identical (how else could you memoize new'
?). Which isn’t very useful.
My expectation is that every new Ref I make is unique.
At the same time, it doesn’t make sense for Ref a
to have an Eq
instance
If it were possible to inspect the token, then token equality would be Ref equality? I would think that two refs with the same token, but different values should be a bug. Two Refs with different tokens may hold equal values for a time but are fundamentally not the same thing.
1 Like
Ref can’t be created purely because you need a value restriction (see any ML which has refs). If it’s allowed to be “pure”, then you can implement unsafe coerce.
2 Likes
To be clear:
maybeRef :: forall a. Ref (Maybe a)
maybeRef = new Nothing
maybeString = maybeRef :: Ref (Maybe String)
maybeInt = maybeRef :: Ref (Maybe Int)
example = do
Ref.write (Just "ok") maybeString
someInt <- Ref.read maybeInt
pure $ add 1 <$> someInt
The type with Effect
restricts it so that the result can’t itself be polymorphic, replicating the ML value restriction.
In order to be pure and sound, type abstraction through forall
would have to be runtime relevant, and actually introduce a new reference when applying a type.
8 Likes
Exactly. You can also read it in the documentation of GHC’s unsafePerformIO
: System.IO.Unsafe
1 Like