Skip to content

Add Resource.unsafeFromSystemIOHandle#428

Merged
aspiwack merged 1 commit intomasterfrom
unsafeFromSystemIOHandle
Oct 25, 2022
Merged

Add Resource.unsafeFromSystemIOHandle#428
aspiwack merged 1 commit intomasterfrom
unsafeFromSystemIOHandle

Conversation

@aspiwack
Copy link
Copy Markdown
Member

Resource.unsafeFromSystemIOHandle lets you convert traditional IO
functions on Handle into resource-aware function.

This also let me clean up the internal definition of handle-based
functions. I took the opportunity to remove auxiliary function and use
the multiplicity-polymorphic flip that the comments were asking for.

Closes #424. Cc @endgame .

@aspiwack aspiwack requested a review from tbagrel1 August 29, 2022 08:43
Copy link
Copy Markdown
Contributor

@endgame endgame left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks cool. There are a couple of operations we cannot do with the new functions:

  • lift an IO System.Handle to a RIO Handle
  • lift an action like hClose to RIO and consume the safe Handle.

Basically, do we want Handle-specific versions of unsafeAcquire and unsafeRelease?

Copy link
Copy Markdown
Member

@tbagrel1 tbagrel1 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left a few questions/suggestions, but the PR looks good to me!

@@ -129,50 +140,34 @@ hClose :: Handle %1 -> RIO ()
hClose (Handle h) = unsafeRelease h
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

two lines above (l. 137, but I couldn't comment there in the review pane), we use Control.pure, and a few lines before, we use Control.return. Also both openFile and openBinaryFile look very similar, maybe we could factor them as part of this handle functions overhaul?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a little overkill. They are both pretty small functions.

@aspiwack
Copy link
Copy Markdown
Member Author

@endgame

Basically, do we want Handle-specific versions of unsafeAcquire and unsafeRelease?

The Handle-specific version of unsafeRelease is hClose. I haven't thought of something for unsafeAcquire. Maybe we should?

@endgame
Copy link
Copy Markdown
Contributor

endgame commented Aug 29, 2022

Basically, do we want Handle-specific versions of unsafeAcquire and unsafeRelease?
The Handle-specific version of unsafeRelease is hClose.

Yes, I can see it now - thanks.

I haven't thought of something for unsafeAcquire. Maybe we should?

If linear-base had this, the lack of openBinaryFile would not have blocked me. (Technically, I could have coerced everything to make it do what I want, but that felt so bad as a library consumer that I considered it a blocker.) For handles there are more functions like mkFileHandle and mkDuplexHandle that can create them (admittedly, they are very low-level).

I'm not sure exactly what it would look like, but perhaps it's worth trying to think more generally than just RIO Handles - a programmer using linear-base will want to wrap his or her UnsafeResource a in a newtype and provide:

  • One or more creation functions, coercing/lifting unsafeAcquire;
  • One or more destruction functions, doing the same to unsafeRelease;
  • Wrapped versions of primitive IO functions on the resource.

Should we provide a toolkit for these, maybe in terms of Coercible (UnsafeResource a) r =>, to make writing linear wrappers easier?

@endgame
Copy link
Copy Markdown
Contributor

endgame commented Sep 5, 2022

Should we provide a toolkit for these, maybe in terms of Coercible (UnsafeResource a) r =>, to make writing linear wrappers easier?

To elaborate on this point: I was hoping that we could do something like generalise unsafeRelease to something like unsafeRelease :: Coercible r (UnsafeResource a) => r %1 -> RIO (). In the module where a library programmer defines a newtype over UnsafeResource a, the Coercibleinstance would be in scope, but since the constructor wrapping UnsafeResource a is often not exported, library clients do not have access to the Coercible instance. Sadly, there are a couple of problems:

  1. This needs -XAllowAmbiguousTypes, so all ergonomic benefit is lost; and
  2. Even in a simple case like unsafeRelease, I cannot prove to the compiler that the uncoerced resource is fully consumed.

I can't think of a good way to lift the UnsafeResource a toolkit to whatever newtype the programmer comes up with, so maybe we don't worry about it for now?

@endgame
Copy link
Copy Markdown
Contributor

endgame commented Sep 5, 2022

Meta: I am also a little thrown by the way that functions that return (result, resource) tuples return the result in the left side of the tuple. Example: hGetChar :: Handle %1 -> RIO (Ur Bool, Handle). This means that I cannot fmap over the result of such a computation, which I find a bit unergonomic. Is there a reason these libraries return the resource in the RHS of tuples?

@aspiwack
Copy link
Copy Markdown
Member Author

aspiwack commented Sep 6, 2022

No, I probably just didn't pay attention. I'm ok to change it. But it's a breaking change, so it needs to be advertised.

@aspiwack
Copy link
Copy Markdown
Member Author

aspiwack commented Sep 6, 2022

To elaborate on this point: I was hoping that we could do something like generalise unsafeRelease to something like unsafeRelease :: Coercible r (UnsafeResource a) => r %1 -> RIO (). In the module where a library programmer defines a newtype over UnsafeResource a, the Coercibleinstance would be in scope, but since the constructor wrapping UnsafeResource a is often not exported, library clients do not have access to the Coercible instance. Sadly, there are a couple of problems:

It's no different than the current situation: you need to do an unsafeFoo for each of the resources that you export anyway.

@endgame
Copy link
Copy Markdown
Contributor

endgame commented Sep 10, 2022

Scanning over the discussions, there are a few threads here. I will summarise them all so that this can hopefully get merged:

  1. A way to lift Handle-creating functions into RIO. I think this should happen in this PR, in case there are other ways of creating Handles that people might want to lift.
  2. Nitpicking around using flip or not to tidy up the lifted functions. I agree with @tbagrel1 that the flip-less versions look nicer.
  3. Decide whether or not to switch the return types to RIO (Handle, x) for the sake of the tuple's Functor instance. This discussion (and maybe PR) should happen in a separate issue, and ideally before 0.3.0 since we're shipping breaking changes anyway.

@endgame
Copy link
Copy Markdown
Contributor

endgame commented Sep 10, 2022

3. Decide whether or not to switch the return types to RIO (Handle, x) for the sake of the tuple's Functor instance.

I will not be making a bug for this - it would make StateT much more difficult to use, and I think that's too great a cost (in addition to all the routine breakage triggered by such a swap).

@aspiwack
Copy link
Copy Markdown
Member Author

I will not be making a bug for this - it would make StateT much more difficult to use, and I think that's too great a cost (in addition to all the routine breakage triggered by such a swap).

Oh yes, it is the StateT order, so maybe I did do this on purpose. I can never remember which component the state goes in.

@aspiwack
Copy link
Copy Markdown
Member Author

Thanks for the discussion, even if I took so much time to get back to it.

After all this I came to the conclusion that I've been going at this all wrong. Namely it is not true that using UnsafeResource Handle directly is unsafe. What is true is that all the API functions that access UnsafeResource a generically are all unsafe function. But nothing wrong happens if I make Handle transparent: as long as you only use safe functions, you can't do anything wrong: your resources will be properly release.

Therefore, here is my work plan:

  • Rename UnsafeResource to Resource
  • Make a type alias type UnsafeResource = Resource and mark it as duplicated
  • Make Handle a type alias type Handle = Resource System.Handle
  • Don't publish specialised unsafe functions for Handle as the generic one will work.

data UnsafeResource a where
UnsafeResource :: Int -> a -> UnsafeResource a
data Resource a where
UnsafeResource :: Int -> a -> Resource a
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the constructor is not exported, does that mean it's safe to rename?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is safe to rename. But I like how the name emphasises that accessing this constructor is very unsafe and needs to be done with care. What would you like to see instead?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that makes sense. Just checking.

@@ -201,7 +202,7 @@ unsafeRelease (UnsafeResource key _) = RIO (\st -> Linear.mask_ (releaseWith key
unsafeAcquire ::
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the docstring calling this unsafe still correct given your new posture towards resources?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I consider it is an unsafe function in the usual sense in Haskell: it has conditions for application which can't be proved by the compiler, hence are the responsibility of the programmer.

Though I must admit that ResourceT has the same function and considers it safe. So there is certainly a question.

-- | Specialised alias for 'release'
hClose :: Handle %1 -> RIO ()
hClose (Handle h) = unsafeRelease h
hClose = release
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deprecate this? Not with a view to ever removing it, but programmers put hClose in their programs, then get told to use release, and then learn not to provide aliases in their own libraries?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know that I want to deprecate this. I would if I thought using hClose is less good than using release. But I'm not sure that it is, just it's ok that we have return despite it being a specialisation of pure.

Copy link
Copy Markdown
Member Author

@aspiwack aspiwack left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @endgame for the very prompt review. Any further thought? When we're happy, I'll merge and make a release.

-- | Specialised alias for 'release'
hClose :: Handle %1 -> RIO ()
hClose (Handle h) = unsafeRelease h
hClose = release
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know that I want to deprecate this. I would if I thought using hClose is less good than using release. But I'm not sure that it is, just it's ok that we have return despite it being a specialisation of pure.

@@ -201,7 +202,7 @@ unsafeRelease (UnsafeResource key _) = RIO (\st -> Linear.mask_ (releaseWith key
unsafeAcquire ::
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I consider it is an unsafe function in the usual sense in Haskell: it has conditions for application which can't be proved by the compiler, hence are the responsibility of the programmer.

Though I must admit that ResourceT has the same function and considers it safe. So there is certainly a question.

data UnsafeResource a where
UnsafeResource :: Int -> a -> UnsafeResource a
data Resource a where
UnsafeResource :: Int -> a -> Resource a
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is safe to rename. But I like how the name emphasises that accessing this constructor is very unsafe and needs to be done with care. What would you like to see instead?

@endgame
Copy link
Copy Markdown
Contributor

endgame commented Oct 25, 2022

I have nothing further to add, and I don't know when I'll next try linear stuff in a project. Probably best to get this out as-is and see if further stuff emerges later.

Having `Handle` be transparent lets you convert traditional IO
functions on `Handle` into resource-aware function. It would use the
unsafe API, of course. But it's better than the previous situation.

My initial assumption that because every function on
`UnsafeResource` (now rename `Resource`) was unsafe, then I need concrete
resource types to be opaque was simply faulty.

Closes #424.
@aspiwack aspiwack force-pushed the unsafeFromSystemIOHandle branch from 9c1ff3a to 1e290fc Compare October 25, 2022 16:55
@aspiwack aspiwack enabled auto-merge October 25, 2022 16:56
@aspiwack
Copy link
Copy Markdown
Member Author

I've squashed the history as the intermediate commits were not relevant anymore. This will automerge when CI is green.

I'll make a release in the coming days.

@aspiwack aspiwack merged commit e7f1d0d into master Oct 25, 2022
@aspiwack aspiwack deleted the unsafeFromSystemIOHandle branch October 25, 2022 16:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

How to bring other Handle-using functions into the RIO world?

3 participants