haskell / filepath

Haskell FilePath core library
BSD 3-Clause "New" or "Revised" License
66 stars 33 forks source link

Make osp :: QuasiQuoter valid as a pattern #205

Closed lawbel closed 7 months ago

lawbel commented 7 months ago

I was quite surprised to find that the following code is not valid:

import System.OsPath (OsPath, osp, takeExtension)

data Format = Png | Jpeg

extensionType :: OsPath -> Maybe Format
extensionType path = case takeExtension path of
    [osp|.png|] -> Just Png
    [osp|.jpg|] -> Just Jpeg
    _           -> Nothing

I can see that the osp QuasiQuoter is implemented to only support expressions, but this kind of use case feels very natural. Especially given that it is non-trivial to construct a valid OsPath literal.

Is there any reason why this couldn't or shouldn't be implemented? If no, then I would be more than happy to have a go at it and put together a PR.

hasufell commented 7 months ago

I don't know much about TH. Is this possible?

lawbel commented 7 months ago

I also don't know TH well enough to answer that 😅 I would think that if we can generate expressions, then surely (hopefully) we can use a similar approach to translate those expressions into a sensible pattern.

Bodigrim commented 7 months ago

https://github.com/haskell/filepath/blob/51501dba4f963087eac2b67ed13b64fe0304d63b/System/OsPath/Internal.hs#L130-L140

I imagine one should define quotePat as well.

lawbel commented 7 months ago

I've been working on this, but could do with some guidance on how best to proceed.

So this turns out to be a bit tricky actually, because of what is allowable as a valid pattern. We would need to either drill down into the underlying constructor and match against the payload, or use a view pattern to do some computation.

Using a view pattern is not ideal, because it forces the user to turn on ViewPatterns. But otherwise it is simple to implement and it works.

Getting to the underlying constructor has proven difficult. I understand that OsPath/OsString is ultimately a wrapper around a ByteArray# from base. But ByteArray# is an abstract type that is not matchable on (correct me if I'm wrong?). So this seems to be impossible.

What do you think, go ahead with the view pattern approach?

lawbel commented 7 months ago

On reflection, what we can do is define a pattern synonym (not certain whether it would have to exported to make this work):

pattern Packed :: [OsChar] -> OsPath
pattern Packed chars <- (System.OsPath.unpack -> chars)
    where Packed chars = System.OsPath.pack chars

which hides the view pattern, and then can match on [OsChar] as it's just a [Word8] or a [Word16] depending on platform. That seems like the best option.

Bodigrim commented 7 months ago

I would expect [osp|foo|] as a pattern to generate xs | xs == [osp|foo|]. Is it not feasible?

lawbel commented 7 months ago

@Bodigrim I would like to be be able to do that, but it turns out it isn't possible. If you look at QuasiQuoter you can see that quotePat returns a Pat. There are various options for what sort of pattern a Pat can be, such as

But there is no option to add a pattern guard. I guess the reason is that the pattern we generate should be valid even if it's nested within some other pattern, but guards are only allowed at the top level.

Bodigrim commented 7 months ago

@lawbel ah, I see. I think ViewP is a solid option then: GHC would prompt user to enable ViewPatterns.

lawbel commented 7 months ago

Thanks @Bodigrim for all the help, I put together a PR which implements this as a view pattern.