Skip to content
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

Move ‘dirname’ and ‘parent’ into ‘MonadThrow’ #90

Closed
wants to merge 2 commits into from

Conversation

mrkkrp
Copy link
Collaborator

@mrkkrp mrkkrp commented Mar 19, 2017

Close #18.

@mrkkrp mrkkrp force-pushed the move-dirname-and-parent-to-monad-throw branch from 8bd18e8 to 4b2b0ef Compare March 19, 2017 15:44
@mrkkrp
Copy link
Collaborator Author

mrkkrp commented Mar 19, 2017

@NorfairKing Can you adjust the validity tests?

@NorfairKing
Copy link
Collaborator

Yes! Dirname doesn't have validity tests yet though, so I'll add those.

@NorfairKing
Copy link
Collaborator

@mrkkrp Can you update the docs as well?

@NorfairKing
Copy link
Collaborator

And could you also check the property for 'parent' because it doesn't seem to hold:

     it "(parent (x </> y) == x)" $ do                                                                      
        forAllShrink genValid shrinkValidAbsDir $ \x ->                                                     
            forAllShrink genValid shrinkValidRelFile $ \y ->                                                
                parent (x </> y) == Just x                 
  1) Operations: parent (parent (x </> y) == x)
       Falsifiable (after 5 tests and 5 shrinks): 
       "/"
       "a/a"

@mrkkrp
Copy link
Collaborator Author

mrkkrp commented Mar 19, 2017

parent is still idempotent, so it does not need to be in MonadThrow. We should probably either make it throw when it's applied to "/" paths or leave it as is and move it outside of MonadThrow... I think in #33 we concluded that it should throw on "/". I'll implement this now.

@mrkkrp
Copy link
Collaborator Author

mrkkrp commented Mar 19, 2017

Does everyone agree with the decision about parent? I think we should add Couldn'tTakeParentOfRoot data constructor to PathParseException. What do you think?

@NorfairKing
Copy link
Collaborator

Sounds great! Feel free to add the tests as well then .

src/Path.hs Outdated
Path (normalizeDir (FilePath.takeDirectory (FilePath.dropTrailingPathSeparator fp)))
parent :: MonadThrow m => Path Abs t -> m (Path Abs Dir)
parent = parseAbsDir
. normalizeDir
Copy link
Member

Choose a reason for hiding this comment

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

I suspect that this line is redundant. Would you mind removing it and checking if everything still seems to work as expected?

@sjakobi
Copy link
Member

sjakobi commented Mar 20, 2017

Does everyone agree with the decision about parent?

I do.

I think we should add Couldn'tTakeParentOfRoot data constructor to PathParseException. What do you think?

That constructor doesn't really sound like a "parse exception" but PathParseException is already a mixed bag so I don't mind adding it there.


Can you add tests for when parent and dirname fail please?!

@sjakobi
Copy link
Member

sjakobi commented Mar 20, 2017

Oh, and how about adding @since annotations on dirname and parent? IMO that makes sense for a breaking type change.

BTW in the changelog you can substitute UNRELEASED with 0.6.0 now that we have a breaking change.

@mrkkrp
Copy link
Collaborator Author

mrkkrp commented Mar 22, 2017

AppVeyor fails with a weird message (doesn't know what curl is!).

@sjakobi
Copy link
Member

sjakobi commented Mar 22, 2017

AppVeyor fails with a weird message (doesn't know what curl is!).

Seems like a new AppVeyor issue – can you apply the workaround here?

test/Posix.hs Outdated
it "parent (parent \"\") == \"\""
((parent $(mkAbsDir "/") >>= parent) `shouldReturn`
$(mkAbsDir "/"))
it "cannot take the parent of the root directory" $
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

We should instead use shouldThrow here and check that it throws the correct thing, not discard the info checking against Nothing. I'll push a fix.

Copy link
Collaborator

Choose a reason for hiding this comment

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

The constructors of the exception are not supported, are they?
If they are, then I missed that :)

@mrkkrp
Copy link
Collaborator Author

mrkkrp commented Mar 26, 2017

It looks like we don't export constructors of PathParseException. How about exposing them?

@NorfairKing
Copy link
Collaborator

How about exposing them?

Yes please.

@mrkkrp mrkkrp force-pushed the move-dirname-and-parent-to-monad-throw branch from 244772c to 19ca02f Compare March 26, 2017 18:32
@mrkkrp
Copy link
Collaborator Author

mrkkrp commented Mar 26, 2017

@sjakobi Good to go now, what do you think?

@NorfairKing
Copy link
Collaborator

@mrkkrp also do that on windows then.
I also think that using 'Either' would be more appropriate in the tests.
.. and you might want to add this test for windows as well: ba591bb

@mrkkrp
Copy link
Collaborator Author

mrkkrp commented Mar 26, 2017

also do that on windows then.

Do what? The tests on Posix and Windows are identical now I think, except for ba591bb.

Oh, yes they are. My bad.

I also think that using 'Either' would be more appropriate in the tests.

Again, not sure what you mean by this.

parent $(mkAbsDir "/") `shouldBe` Right (Couldn'tTakeParent "/")

@NorfairKing
Copy link
Collaborator

Otherwise, LGTM

@mrkkrp
Copy link
Collaborator Author

mrkkrp commented Mar 26, 2017

Please don't add your answers in my post by editing it, it's confusing. And this

parent $(mkAbsDir "/") `shouldBe` Right (Couldn'tTakeParent "/")

is not going to work:

 (~) * e SomeException => MonadThrow (Either e)

So it will be Left with SomeException inside, which we will need to convert to PathParseException in order to check for equality. Nothing nice in that approach.

@NorfairKing
Copy link
Collaborator

Please don't add your answers in my post by editing it, it's confusing.

Oh dear! I didn't know I was doing that. I'm so sorry!

So it will be Left with SomeException inside, which we will need to convert to PathParseException in order to check for equality. Nothing nice in that approach.

Oh, right. Carry on! LGTM from me :)

@mrkkrp mrkkrp force-pushed the move-dirname-and-parent-to-monad-throw branch from e676f1e to 54a6587 Compare March 27, 2017 12:06
@sjakobi
Copy link
Member

sjakobi commented Mar 27, 2017

I'm sorry that this comes so late, but I've got some doubts about our (type) design here:

  • I think that for Path Abs File we should have a pure parent function.
    What do you think about replacing parent with these functions?

    dirParent :: MonadThrow m => Path b Dir -> m (Path b Dir)
    fileParent :: Path Abs File -> Path Abs Dir
    

    Once we can represent the current directory (.) as a Path Rel Dir we could generalize fileParent to Path b File -> Path b Dir.

    With this design we could even keep the old parent function although we should document its inconsistency with isParentOf.

  • I think that throwing InvalidRelDir from dirname "breaks the abstraction". It also seems to me that we'll always throw InvalidRelDir "" there which will make it difficult for users to find the cause of the exception.
    How about having a Couldn'tTakeDirname HasNoDirname constructor instead?

cc @harendra-kumar

@NorfairKing
Copy link
Collaborator

I think that for Path Abs File we should have a pure parent function.

+1

@harendra-kumar
Copy link
Collaborator

@mrkkrp @sjakobi PR #35 which was reviewed by @chrisdone long ago but was never merged (I guess he got busy), fixes the parent related part in this PR. There may be a few things from the changes in that PR that may be worth taking or maybe you can merge that one first. That PR changed the exception naming as well, you may want to take a look at the review comments in that one.

I agree InvalidRelDir thrown from dirname is confusing, we should perhaps have a specific exception for dirname. BTW, I had another suggestion for fixing dirname in #35, a little unusual but works. With #34 we have an empty path as identity of concatenation for Path Rel Dir, the same can be used as dirname for /. In other words, dirname / could be . because /. is same as / and a/. is same as a/.

@sjakobi
Copy link
Member

sjakobi commented Mar 27, 2017

dirname / could be .

That just doesn't fit with my intuition of what a "directory name" is. I really think that dirname / should fail.

@harendra-kumar
Copy link
Collaborator

harendra-kumar commented Apr 2, 2017

I will say don't add them (i.e. fileParent and dirParent) now, adding later is always easier, if we really want to, but removing later is painful.

@NorfairKing
Copy link
Collaborator

NorfairKing commented Apr 3, 2017

If we don't split parent now, and we move it into MonadThrow, then splitting it into fileParent and dirParent will require ANOTHER major release.
I really need fileParent in the case of file to be pure.
We could keep parent as is (including it's functionality) and also add fileParent and dirParent. I would be fine with that, but we have already established that parent is broken, so it doesn't sound great.

However, if we do split then we should perhaps keep parent as well for the case when you do not care whether the path is file or dir.

You never "don't care" whether a path is a file or a dir, you can only "not know" and write a function that works for both. If you use parent for that, it's already been established that your function will be broken.
There is simply no way to write the function you are describing correctly, if parent is not in MonadThrow.

[...] because the added cognitive overhead by splitting cancels the advantage of not having a MonadThrow in case of a file

I disagree entirely. The opposite is true, in my opinion. The cognitive overhead of not splitting parent is not worth it.
I want to be able to write pure code that uses fileParent. If this library is going to force me to write the following code, then something is very wrong:

case parent myFile of
    Nothing -> error "cannot happen anyway"
    Just parentOfMyFile -> pure $ myFunc parentOfMyFile

Types should not lie.

@mrkkrp
Copy link
Collaborator Author

mrkkrp commented Apr 3, 2017

If we move parent into MonadThrow then it's not broken anymore and there is no problem with it being in the library. parent already works as dirParent, so dirParent (if we choose to keep parent) is redundant. We can add fileParent like this:

fileParent :: Path Abs File -> Path Abs Dir
fileParent = fromJust . parent

But again if you need it so badly you can do it yourself in your code. And yes fromJust is OK here, it's silly to be some sort of purist and reject solutions that work perferctly in all cases only because somewhere is branch of execution that is never reached there is an error.

@NorfairKing
Copy link
Collaborator

We can add fileParent like this [...]

I'm okay with that.

The difference is who maintains it. As long as fileParent is in path and not in my own code, I'm happy with it. fromJust or not, that's an implementation detail, as long as it's tested well.

@NorfairKing
Copy link
Collaborator

Are we ready to merge this then?

@mrkkrp
Copy link
Collaborator Author

mrkkrp commented Apr 4, 2017

I think we're close. @sjakobi mentioned that he suspects that dirname will always throw the same exception: InvalidRelDir "". We should check this and then it will be ready. I can check that tomorrow.

@harendra-kumar
Copy link
Collaborator

With this fix we are moving towards an unnecessarily complex API. Let me explain.

Just like the POSIX standard path API, our path API could be as simple as just two pure functions i.e. parent (called dirname in POSIX) and basename (we have dirname and filename which can be easily combined together in basename). So in entirety we have:

(</>) :: Path b Dir -> Path Rel t -> Path b t
parent   :: Path b t -> Path b Dir
basename :: Path b t -> Path Rel t

This is succinct and simple API, look how the signatures of all three fit in. And, we have the following simple laws too:

parent x </> basename x == x
basename (x </> y) == basename y
parent (x </> basename y) == x

With MonadThrow instances and splitting the API we are moving away from this simpler picture and towards a more ad-hoc API. To be able to keep this simpler picture I propose the following semantics for the special root case, assuming we also have the change to represent the empty path or ".":

parent "/" = "/"
basename "/" = "."

With these semantics all the laws are elegantly satisfied and everything is still pure. We already have the first of these, the only change we need is the second one. We discussed this earlier above and dismissed as non-intuitive, but it makes sense and fulfills the laws. If you think about it, the basename of "/" is in fact an empty name and that is what we represent with "." (it is Path []). You just have to reconcile with this one thing and we have a simple, law abiding and beautiful API which also happens to be inline with the POSIX standard. These semantics take care of #18, of course.

I request all the maintainers to take another look at this and strive to keep the API as simple as possible, since this is a fundamental library for path manipulations. I use and care about this library, so I tried to make a bit more effort to convince you guys.

@harendra-kumar
Copy link
Collaborator

BTW, I have a change that combines dirname and filename into a polymorphic basename API using a type class.

@NorfairKing
Copy link
Collaborator

I'm sort-of okay with basename "/" = ".", not so much with parent "/" = "/".
I'm also unsure about the type class.
Other than that, I could be convinced.
Would like to hear other opinions.

@joehillen
Copy link
Contributor

What is wrong with parent "/" = "/"?

Seems perfectly reasonable to me. If you type cd .. in the shell at /, it sends you to /.

@NorfairKing
Copy link
Collaborator

What is wrong with parent "/" = "/"?

I can get my head around it, as long as isParentOf is consistent. see #33.
As for basename "/" = ".", that would solve #18 without MonadThrow 👍, but be inconsistent with POSIX (which I'm okay with, just noteworthy):

$ basename /
/

As for the typeclass: I think this will create more problems than it solves.

@joehillen
Copy link
Contributor

As for the typeclass: I think this will create more problems than it solves.

Such as? I've never head of a typeclass creating problems.

@NorfairKing
Copy link
Collaborator

As for the typeclass: I think this will create more problems than it solves.

Such as? I've never head of a typeclass creating problems.

  1. Error messages, now you'll get 'no instance of WhateverClassWeUseversusit is not a File`
  2. Ambiguities: Type classes can counteract type inference in some cases.
  3. Unnecessary complexity (in this case) because you mostly either deal with a file or a path, not both.

@harendra-kumar
Copy link
Collaborator

Making basename polymorphic is not really the core issue here, we can discuss that in a separate issue so that we do not digress. Let me answer the other question about parent "/" = "/". This is POSIX standard convention.

We are (rather incorrectly) calling the POSIX dirname as parent. If the meaning of parent were really parent then the parent of /x/y/z should have been the parent of /. What we mean instead is the POSIX dirname equivalent i.e. everything except the last component of /x/y/z which is /x/y.

If we use the same dirname intuition for /, then the dirname of / being / makes more sense. If we consider the full name of "/" as "/." instead (which is equivalent) then the dir part becomes / and the base part becomes ., fitting into our story. Though as @NorfairKing noted, making "." the basename of / is not the same as POSIX but that's because POSIX does not distinguish between Rel and Abs types and we have to!

isParentOf and parent are not really semantically related, if they were we could say isParentOf p x = p == parent x but that's not true. The misuse of the name parent is at the core of its inconsistency with isParentOf as well. We should rename isParentOf to isPrefixOf or something like that and it will become clearer that it need not be consistent with parent which in turn should be renamed to dirname.

@NorfairKing
Copy link
Collaborator

Making basename polymorphic is not really the core issue here, we can discuss that in a separate issue so that we do not digress. Let me answer the other question about parent "/" = "/". This is POSIX standard convention.

👍

The misuse of the name parent is at the core of its inconsistency with isParentOf as well.

Should we rename it? (let's make that a seperate issue, if so)

it will become clearer that it need not be consistent with parent

I disagree, it still needs to be consistent. Every parent is a prefix.

By the way, nothing says that we have to make the API look like the shell equivalent.

What do you suggest then, concretely and stepwise?

@harendra-kumar
Copy link
Collaborator

Should we rename it? (let's make that a seperate issue, if so)

isAncestorOf or isDirPrefixOf might convey the meaning more effectively. But let's defer it to another issue.

I disagree, it still needs to be consistent. Every parent is a prefix.

If that is desired we can fix isParentOf so that it treats "/" as a prefix of "/", the same way we treat "/" as parent of "/".

By the way, nothing says that we have to make the API look like the shell equivalent.

Yes, of course. But as long as there is no other serious conflict it is always helpful to reuse an established and popular API. One, that API has been through the test of time, reviewed and used by countless people. Two, it helps people who have gotten used to it, for example initially I always made the mistake of using dirname in the path package with the POSIX semantics only to later realize it is different.

What do you suggest then, concretely and stepwise?

To Fix #18:

  1. Do not merge this PR (we retain the current parent "/" = "/" behavior). Drop Make parent and isParentOf consistent (fixes #33) #35 as well which is a similar PR pending merge.
  2. merge Allow . as a relative path #34 (Allow . as a relative path)
  3. Allow dirname "/" = ".", this completes the fix for Breaking the guarantees of 'Path Rel Dir' #18

Nice to have's, issues to be raised/discussed:

  1. As discussed above, fixing parent and dirrename should return In MonadThrow m #33 is optional, if we want to, then we can just fix isParentOf for the root dir/drive case.
  2. renaming of isParentOf
  3. having a polymorphic basename

@NorfairKing
Copy link
Collaborator

I'm okay with this plan, but I want to hear others' opinion. (I thought @chrisdone didn't like this approach.)

@sjakobi
Copy link
Member

sjakobi commented Apr 8, 2017

I'm ok with keeping parent :: Path Abs t -> Path Abs Dir and parent "/" = "/". It's sad that so much work won't be merged but IMO it's better to half-heartedly not make a change than to make a half-hearted change.

I'm still rather -1 on dirname "/" = ".". With #34 we'd then also have dirname "." = "." which IMO marks a shift from a non-intuitive edge case to a non-intuitive design flaw.

@NorfairKing
Copy link
Collaborator

it's better to half-heartedly not make a change than to make a half-hearted change.

👍

I'm still rather -1 on

I'm wondering if it would be better to offer both API's?

@joehillen
Copy link
Contributor

I'm wondering if it would be better to offer both API's?

I like that idea. It allows users to decide which behavior is desirable.

@harendra-kumar
Copy link
Collaborator

I'm still rather -1 on dirname "/" = ".". With #34 we'd then also have dirname "." = "." which IMO
marks a shift from a non-intuitive edge case to a non-intuitive design flaw.

One way to think about it is / being the root for absolute paths (AbsRoot) and . being the root for relative paths (RelRoot). You cannot go beyond the root in each case. The representation of the root has to be different because they are different types.

It does not sound intuitive because the representation "." is overloaded with two meanings i.e. the RelRoot as well as the identity of concatenation for relative paths. Conceptually if we write it like this, then it may look more intuitive:

dirname AbsRoot = Identity
dirname RelRoot = Identity

We can conclusively establish that it is a design flaw if we can find one practical problem that it causes and is against the accepted uses of "." and "/".

@NorfairKing NorfairKing removed their assignment Apr 10, 2017
@mrkkrp mrkkrp mentioned this pull request May 28, 2017
@mrkkrp
Copy link
Collaborator Author

mrkkrp commented Jun 6, 2017

Closed in favor of #34.

@mrkkrp mrkkrp closed this Jun 6, 2017
@mrkkrp mrkkrp deleted the move-dirname-and-parent-to-monad-throw branch June 6, 2017 11:35
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.

None yet

5 participants