Skip to content

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

Merged
merged 25 commits into from
Sep 27, 2020
Merged

Expand on documentation #217

merged 25 commits into from
Sep 27, 2020

Conversation

JordanMartinez
Copy link
Contributor

Highlights include:

  • Explaining that the Array monad is a nested for loop
  • Explaining why Monoid and Semigroup have newtypes and why they exist
  • Added a brief overview of "do notation" and "ado notation" with comprehensive examples
  • Expand on documentation for Void and distinguish it from C-like languages' void
  • Provide example of the function <$> arg1 <*> arg2 ... <*> argN pattern

-- | 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.

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?

Copy link
Contributor Author

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.

Copy link

@natefaubion natefaubion May 29, 2020

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.

Copy link
Contributor Author

@JordanMartinez JordanMartinez May 29, 2020

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.

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.

Copy link
Contributor Author

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.

Copy link
Contributor Author

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.

@JordanMartinez
Copy link
Contributor Author

I also moved the explanation for Array's "do notation" into the Bind instance due to Harry's comments in the purescript-either PR linked above.

Copy link
Contributor

@hdgarrood hdgarrood left a comment

Choose a reason for hiding this comment

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

Thanks for this!

@@ -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
Copy link
Contributor

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.

Copy link
Contributor Author

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.

-- | foo = do
-- | eachElementInArray1 <- [0, 1]
-- | eachElementInArray2 <- [1, 2]
-- | pure (eachElementInArray1 + eachElementInArray2)
Copy link
Contributor

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.

Copy link
Contributor Author

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
Copy link
Contributor

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).

Copy link
Contributor Author

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?

Copy link
Contributor

Choose a reason for hiding this comment

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

This looks great 👍

-- |
-- | 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`.
Copy link
Contributor

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`.
Copy link
Contributor

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.

Copy link
Contributor Author

@JordanMartinez JordanMartinez Sep 26, 2020

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.

Copy link
Contributor

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.

Copy link
Contributor

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.

-- |
-- | `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:
Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I used C-family

-- |
-- | `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
Copy link
Contributor

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..."?

Copy link
Contributor Author

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.

-- | ```
-- |
-- | In PureScript, one often uses `Unit` to achieve similar effects as
-- | the lowercased `void` above.
Copy link
Contributor

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.

Copy link
Contributor Author

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
Copy link
Contributor

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 👍

-- | `bind`/`>>=` adds another level of nesting in the loop:
-- | ```
-- | foo :: Array Int
-- | foo = do
Copy link
Contributor

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

-- | ["c", "d"] >>= \eachElementInArray2
-- | pure (eachElementInArray1 <> eachElementInArray2)
-- |
-- | foo == [ ("a" <> "c"), ("a" <> "d"), ("b" <> "c"), ("b" <> "d") == [ "ac", "ad", "bc", "bd"]
Copy link
Contributor

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 ].

Copy link
Contributor Author

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" ]

@@ -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
Copy link
Contributor

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's bind works like a nested for loop; each bind adds another level of nesting in the loop:

Copy link
Contributor Author

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.

Copy link
Contributor

@hdgarrood hdgarrood left a 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 :)

@JordanMartinez
Copy link
Contributor Author

Thanks for being patient with my many change suggestions :)

No worries!

@JordanMartinez
Copy link
Contributor Author

The build is failing because of the v0.14.0-compatibility changes I made a while back.

@hdgarrood
Copy link
Contributor

I'd like to get #225 merged next as that should fix CI.

@hdgarrood hdgarrood merged commit 85aa2db into purescript:master Sep 27, 2020
@JordanMartinez JordanMartinez deleted the addDocs branch September 24, 2021 14:37
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.

3 participants