-
Notifications
You must be signed in to change notification settings - Fork 90
Expand on documentation #217
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
Conversation
src/Control/Monad.purs
Outdated
-- | Note: if one wants to use "do notation" but redefine what the in-scope | ||
-- | definitions are for `bind` and `pure` in a given context, one can use | ||
-- | "qualified do notation." This is an intermediate/advanced language feature | ||
-- | not explained here. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure that Monad
is the best place to put documentation for do-notation. There isn't anything requiring a Monad
instance in order to use it. This is a common Haskellism, since Haskell requires it, but if you already know to look for that, is this communicating new information?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought it might be helpful for new users who are learning FP for the first time.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I understand, and I don't think your intentions are misguided. I'm also not trying to say that the information isn't useful. My point is that core library documentation should strive for correctness. It is not correct to say that do-notation has anything to do with Monad
(unlike Applicative-do, which certainly requires Applicative
), even if we colloquially tie the two together. It's a common case that when you use do-notation, you likely also have a Monad instance, but this is not a constraint of the language or compiler. If I were a newcomer looking for information on do-notation, it is not clear to me that the Monad
typeclass documentation is the best place to put this information. It also is not clear to me that a newcomer is likely to encounter do-syntax for the first time by reading the documentation for Monad
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's a common case that when you use do-notation, you likely also have a Monad instance, but this is not a constraint of the language or compiler.
Ah... because you can also do what I recall that you do with do
(wow... that was a convoluted sentence 😆 ):
foo = do
let x = 5 + 2
5 * x
where "do notation" is a somewhat cleaner way of writing "let-in" blocks.
If I were a newcomer looking for information on do-notation, it is not clear to me that the Monad typeclass documentation is the best place to put this information. It also is not clear to me that a newcomer is likely to encounter do-syntax for the first time by reading the documentation for Monad.
Good points! I thought having it here would be nice because one could find something like it in Pursuit when they search for things. However, I agree that there are other forms of documentation (e.g. the book, my repo) that cover it already and might be something new users consult first.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I should also note that what I said was incorrect as well. Applicative-do does not "certainly" require Applicative
, since it just inserts calls to apply
and pure
in scope, even if the common use is with the Applicative
class.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm fine with removing both of these do/ado notation docs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've removed the do/ado notation explanations in latest commits.
I also moved the explanation for Array's "do notation" into the Bind instance due to Harry's comments in the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for this!
src/Control/Bind.purs
Outdated
@@ -63,6 +63,17 @@ infixr 1 bindFlipped as =<< | |||
instance bindFn :: Bind ((->) r) where | |||
bind m f x = f (m x) x | |||
|
|||
-- | The `Array` monad's "do notation" works like a nested for loop. Each |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would be more comfortable with an explanation which doesn't refer to do notation here - or, at least, start with one which doesn't refer to do notation, and then add the part about do notation afterwards. That way, at least a part of these docs will make sense to people who don't yet understand the do notation desugaring. I think it's common to use Array's Bind instance explicitly via >>=
rather than within a do
block as well, which is another reason to understand how Bind Array works outside of the context of do notation.
I think something similar to https://pursuit.purescript.org/packages/purescript-arrays/5.3.1/docs/Data.Array#v:concatMap would be good - although for prelude, if we can avoid referring to downstream libraries in the examples, that would be preferable. We could also add a note to concatMap
saying it's the same as bind
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point. I've updated that.
src/Control/Bind.purs
Outdated
-- | foo = do | ||
-- | eachElementInArray1 <- [0, 1] | ||
-- | eachElementInArray2 <- [1, 2] | ||
-- | pure (eachElementInArray1 + eachElementInArray2) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this may be clearer if we use a different operation here. How about replacing [0,1]
with ["A", "B"]
and [1,2]
with ["C","D"]
, and then combining them with <>
instead of +
? That way each element of the result array is distinct, which means that it's easier to see the order in which the elements are processed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed! Updated in latest commit.
@@ -28,6 +28,20 @@ import Type.Data.RowList (RLProxy(..)) | |||
-- | `Monoid`s are commonly used as the result of fold operations, where | |||
-- | `<>` is used to combine individual results, and `mempty` gives the result | |||
-- | of folding an empty collection of elements. | |||
-- | | |||
-- | ### Newtypes for Monoid |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should save the specifics of each Monoid-related newtype to the definitions of the newtypes themselves. I suspect it be easier for people to understand these if they can see the rendered definitions alongside the documentation which says how they work, and also this would be very easy to get out of sync since the newtypes themselves live in separate files.
I do think this is a good place to mention the issue that some types permit multiple Monoid instances, and that we can express this with newtypes, but I think it would be best to keep that discussion minimal here. Something like "For example, the Additive newtype has a Monoid instance which is based on Semiring's plus
and zero
, and the Multiplicative newtype has a Monoid instance which is based on Semiring's mul
and one
. There are a number of similar newtypes in the submodules of Data.Monoid in this package.
Also, I realise this is a bit of a nitpick, but I would also prefer not to call it a "workaround", because I think doing this means admitting that not being able to equip a type with two Monoid instances is a deficiency (I don't think it is).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would also prefer not to call it a "workaround"
Agreed.
Does my rendering work? Or are there other changes you would prefer?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks great 👍
src/Data/Monoid.purs
Outdated
-- | | ||
-- | Some types (e.g. `Int`, `Boolean`) can implement multiple law-abiding | ||
-- | instances for `Monoid`. For example, `<>` could be `+` and `mempty` could | ||
-- | be `0`. Likewise, `<>` could be `*` and `mempty` could be `1`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think "Alternatively" or "Equally" might make more sense than "Likewise" here?
-- | There are two other ways to implement an instance for this type class | ||
-- | regardless of which type is used. These instances can be used by | ||
-- | wrapping the values in one of the two newtypes below: | ||
-- | 1. `First` - Use the first argument every time: `append first _ = first`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These instances are a bit weird and somewhat rarely used, so I'm not sure they deserve a mention here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would say they deserve a mention because it mirrors what is said in Monoid
. It's better for a reader to read this, then read the Data.Semigroup.First
documentation, and then think, "I get what Semigroup
and First
are, and First
is not something I need. However, I know where to go if I do need it later" rather than "I get what a Semigroup is" and only later come across First
and have to infer its relation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just think that of all the things we might want to say about semigroups to someone encountering the concept for the first time, I think the First and Last semigroups should be fairly low down on the list, even if their newtypes do happen to live in this repository. While having some level of understanding of the Semigroup type class is more or less required if you want to use PureScript, I would argue that the First and Last semigroups are very much not required. I think it’s more respectful of the user’s time and energy to avoid mentioning things if we don’t expect that they will find them useful at the moment they are reading the docs in question.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Then again, they are simple examples, and giving plenty of simple examples is always an important part of teaching these concepts, so maybe they do deserve a mention here.
src/Data/Void.purs
Outdated
-- | | ||
-- | `Void` is useful to eliminate the possibility of a value being created. | ||
-- | For example, a value of type `Either Void Boolean` can never have | ||
-- | a Left value created in PureScript. | ||
-- | | ||
-- | This should not be confused with the word, `void,` that commonly appears in | ||
-- | C-deriving languages, such as Java: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: I think "C-derived" would be more grammatically appropriate than "C-deriving" here. Or you could use "C-family"? Apparently that's more common now: https://books.google.com/ngrams/graph?corpus=26&smoothing=3&year_start=1800&year_end=2019&content=C-derived+languages%2CC-family+languages&direct_url=t1%3B%2CC%20-%20derived%20languages%3B%2Cc0%3B.t1%3B%2CC%20-%20family%20languages%3B%2Cc0
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I used C-family
src/Data/Void.purs
Outdated
-- | | ||
-- | `Void` is useful to eliminate the possibility of a value being created. | ||
-- | For example, a value of type `Either Void Boolean` can never have | ||
-- | a Left value created in PureScript. | ||
-- | | ||
-- | This should not be confused with the word, `void,` that commonly appears in |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: the comma after "word" shouldn't really be there, and also the comma inside the backticks is a bit awkward. How about "This should not be confused with the keyword void
that commonly appears in..."?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good. See latest commits.
src/Data/Void.purs
Outdated
-- | ``` | ||
-- | | ||
-- | In PureScript, one often uses `Unit` to achieve similar effects as | ||
-- | the lowercased `void` above. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it would be slightly clearer to say "the void
of C-family languages" here, so that the reader doesn't have to double-check the casing to see which void
is which in order to know which one is meant here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point. Done
@@ -18,7 +18,16 @@ import Type.Data.RowList (RLProxy(..)) | |||
-- | - Associativity: `(x <> y) <> z = x <> (y <> z)` | |||
-- | | |||
-- | One example of a `Semigroup` is `String`, with `(<>)` defined as string | |||
-- | concatenation. | |||
-- | concatenation. Another example is `List a`, with `(<>)` defined as |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is definitely a good example to include here 👍
src/Control/Bind.purs
Outdated
-- | `bind`/`>>=` adds another level of nesting in the loop: | ||
-- | ``` | ||
-- | foo :: Array Int | ||
-- | foo = do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Best to remove this do
with this change, I think
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
src/Control/Bind.purs
Outdated
-- | ["c", "d"] >>= \eachElementInArray2 | ||
-- | pure (eachElementInArray1 <> eachElementInArray2) | ||
-- | | ||
-- | foo == [ ("a" <> "c"), ("a" <> "d"), ("b" <> "c"), ("b" <> "d") == [ "ac", "ad", "bc", "bd"] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think at this point the [ ("a" <> "c"), ... ]
is probably just adding noise - part of the reason I suggested changing the operation in this example is so that it could be done away with and we could just write foo == [ "ac", "ad", "bc", "bd" ]
. Also, I think it would be good to have the spaces either side of the [] match - at the moment there is a space after the [ but there is not a space before the ].
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I decided to do this:
-- | -- In other words...
-- | foo
-- | -- ... is the same as...
-- | [ ("a" <> "c"), ("a" <> "d"), ("b" <> "c"), ("b" <> "d") ]
-- | -- which simplifies to...
-- | [ "ac", "ad", "bc", "bd" ]
src/Control/Bind.purs
Outdated
@@ -63,6 +63,17 @@ infixr 1 bindFlipped as =<< | |||
instance bindFn :: Bind ((->) r) where | |||
bind m f x = f (m x) x | |||
|
|||
-- | The `Array` monad works like a nested for loop. Each |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, just to clarify what I was trying to get at before: I think it's best to provide a precise explanation which says what the thing does before trying to build intuition. In this case I think we should follow the lead of the docs in Data.Array.concatMap to start with before mentioning the similarity to a nested for loop. Perhaps:
The
bind
/>>=
function for Array works by applying a function to each element in the array, and flattening the results into a single, new array. Array'sbind
works like a nested for loop; eachbind
adds another level of nesting in the loop:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense. I'll use that rendering.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for being patient with my many change suggestions :)
No worries! |
The build is failing because of the |
I'd like to get #225 merged next as that should fix CI. |
Highlights include:
Array
monad is a nested for loopMonoid
andSemigroup
have newtypes and why they existVoid
and distinguish it from C-like languages'void
function <$> arg1 <*> arg2 ... <*> argN
pattern