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

If Haskell were to have two-fer and it MUST immediately follow hello-world, what would it look like? #813

Closed
petertseng opened this issue Mar 15, 2019 · 6 comments

Comments

@petertseng
Copy link
Member

petertseng commented Mar 15, 2019

There is a possibility that two-fer (https://github.com/exercism/problem-specifications/tree/master/exercises/two-fer) will become recommended for inclusion on all tracks as the exercise immediately following hello-world.

Previously in exercism/problem-specifications#949 (comment) I recommended that Haskell should not do this (and several other languages).

But this was based on a vague feeling that two-fer would be too difficult.
That was pure speculation on my part only, and we never got any concrete examples to evaluate.

I'd like to actually have some concrete examples, so that I can evaluate whether I should continue to stand by the recommendation or if I should instead countermand it.

I sketch out a few examples of some ways of rendering two-fer in Haskell.
For each example, I show what a test for name given and name not given would look like, followed by one or more possible implementations.

Option 1: Maybe

Signature

twoFer :: Maybe String -> String

Tests

twoFer (Just "Alice") `shouldBe` "One for Alice, one for me."
twoFer (Just "Bob") `shouldBe` "One for Bob, one for me."
twoFer Nothing `shouldBe` "One for you, one for me."

Possible implementations

With explicit casing:

twoFer (Just name) = "One for " ++ name ++ ", one for me."
twoFer Nothing = "One for you, one for me."

Some unfortunate repetition there. Repetition can be reduced with something like:

twoFer name = "One for " ++ nameOrYou name ++ ", one for me."
  where nameOrYou (Just name) = name
        nameOrYou Nothing = "you"

Ah, but in fact this Just x -> x; Nothing -> default pattern is called fromMaybe, as follows:

twoFer name = "One for " ++ (fromMaybe "you" name) ++ ", one for me."

Note that student may need to look through http://hackage.haskell.org/package/base/docs/Data-Maybe.html to make that last connection.

Option 2: Sentinel value

Supposing that we treat the empty string as a sentinel value:

Signature

twoFer :: String -> String

Tests

twoFer "Alice" `shouldBe` "One for Alice, one for me."
twoFer "Bob" `shouldBe` "One for Bob, one for me."
twoFer "" `shouldBe` "One for you, one for me."

Possible implementations

Probably something analogous to the above w/ Maybe.
I don't think there's anything similar to fromMaybe, so it'd be something like:

twoFer name = "One for " ++ nameOrYou name ++ ", one for me."
  where nameOrYou "" = "you"
        nameOrYou n = n

It could be inlined as well.

twoFer name = "One for " ++ (if null name then "you" else name) ++ ", one for me."

Option 3: Two different functions

Signature

twoFer :: String -> String
twoFerDefault :: String

Tests

twoFer "Alice" `shouldBe` "One for Alice, one for me."
twoFer "Bob" `shouldBe` "One for Bob, one for me."
twoFerDefault `shouldBe` "One for you, one for me."

Possible implementations

Most implementations of twoFer will probably be the same here:

twoFer name = "One for " ++ name ++ ", one for me."

Two main possibilities for how someone might implement twoFerDefault would be:

With some repetition:

twoFerDefault = "One for you, one for me."

Making it clear that the default is just a specialisation of the general case:

twoFerDefault = twoFer "you"

Option 4: Variadic function (like printf)

I am not prepared to provide an example for this, but I suspect from http://okmij.org/ftp/Haskell/polyvariadic.html#polyvar-fn that it would be too much for a position immediately following hello-world.

Option 5: Your Option Here

The above list may not be exhaustive; the floor is open for other ways of expressing this exercise in the language.

Commentary

My own commentary will come in a comment after this so that the options presented are clearly separate from the commentary.

@petertseng
Copy link
Member Author

Commentary:

I feel a vague unease with suggesting either a sentinel value or a separate twoFerDefault function. Neither seem like the kind of thing that would be recommended in the language in situations where Maybe would do the job.

Note that I similarly advocated for the removal of sumOfMultiplesDefault in #100 (Although note the rationale was different there! it was "This makes the behavior explicit, avoiding surprising behavior and simplifying the problem.") and executed in #101. So twoFerDefault might be seen as going back on that. But it would be for a different reason, so we don't need to immediately reject it out of hand.

So I always thought that we would do this exercise with Maybe. But then I thought that perhaps Maybe is too much for the second exercise. This is wild speculation and worth what you pay for it. It is difficult for me to accurately put myself in the shoes of a learner again. In #475 (comment) I justified this with the observation that Learn You A Haskell waited until Chapter 7 out of 14 to introduce Maybe.

So this is why I currently think I would stand by my recommendation. The possible avenues to convince me otherwise are either:

  • If Maybe is actually not too hard for students to grasp. It is possible we just try it out and see what happens :) Or we can see how much trouble they had with Maybe in the first current core exercise with maybe, and try to guess whether that would be better or worse with two-fer right after hello-world
  • If it is instead the case that one of the non-Maybe approaches is viable.

Can we learn anything from another track that had a similar discussion? Rust track also added two-fer as the exercise immediately following hello-world in exercism/rust#520 , where we chose the sentinel value choice. While we acknowledged that using the sentinel value was unusual in a situation where Option would do, we also thought it was too early to introduce Option. The exercise was subsequently removed after discussion in exercism/rust#552 ; we still agreed that it was too early to introduce Option and we didn't like having a sentinel value.

If an approach such as sentinel value is chosen despite it being unusual, I highly suggest including some explanation (in the tests? in the README?) acknowledging that it is unusual and that a later exercise will teach a more usual way of expressing the absence of a value.

@sshine
Copy link
Contributor

sshine commented Mar 15, 2019

There is a possibility that two-fer [...] will become recommended for inclusion on all tracks as the exercise immediately following hello-world.

So long as it is a recommendation, we are still free to choose that which most benefits the track. There is a difference between observing that two-fer works excellently as a 2nd exercise on a variety of tracks, and concluding that it will work best as a 2nd exercise on all tracks.

I'd like to actually have some concrete examples

of what?

Can we learn anything from another track that had a similar discussion? Rust track also added two-fer [...] where we chose the sentinel value [...] The exercise was subsequently removed [...] it was too early to introduce Option and we didn't like having a sentinel value.

So I think we should stick to Maybe without repeating that process, and for that reason I think we should have the first exercise on Maybe after the first exercise(s) on list/string handling. And because this is currently the 6th exercise on the track, I think it's a little late for two-fer. We could reduce the number of exercises until this point, but I don't think that it's urgent to handle Maybe any earlier.

Our current Maybe exercise is Collatz Conjecture, and the way that its mentor notes encourage giving feedback to this exercise, it's about: error-aware return types, separating concerns into separate functions, implicit recursion and infinite lists (iterate). Even though it's also covered in earlier exercises, it's still an opportunity to suggest guards instead of if-then-else, even rather than mod/rem, and talk about how an accumulating variable in a helper function may be equivalent, in Haskell, because of lazy evaluation. The way I mentor it, I don't focus on all those things every time.

To generalize my statement: In the middle of the track (where a Maybe exercise would currently belong for the reasons stated previously), having exercises that require 2-4 library/helper functions means there's just enough structural complexity to have secondary learning goals without making the exercise too difficult for this reason. (It could of course be "too mathy" independent of its size.)

@NobbZ
Copy link
Member

NobbZ commented Mar 15, 2019

There is a possibility that two-fer (https://github.com/exercism/problem-specifications/tree/master/exercises/two-fer) will become recommended for inclusion on all tracks as the exercise immediately following hello-world.

If this actually is true, then we can simply revert the changes made to hello-world.

hello-world got simplified to a static string and two-fer got extracted because there are many languages in which some pieces of two-fer are quite a challange.

  • Not every language has a notion of a missing argument
  • Not every language has a notion of default values for arguments
  • Not every language can easily manipulate strings
  • Not every language has a notion of the abscense of a value

@sshine
Copy link
Contributor

sshine commented Mar 15, 2019

Since @b-mehta expressed interest in writing an analyser, I would encourage @petertseng's option 1:

twoFer :: Maybe String -> String

because this is the idiomatic way to write such a function.

  • I see no problem in adding a two-fer exercise like that to the Haskell track.
  • The problems related to making two-fer the 2nd core exercise are outlined.

For the purpose of writing an analyser we could:

  • Add two-fer and write an analyser for it, disregard these problems and make it the 2nd core exercise.
  • Add two-fer and write an analyser for it, but not make it the 2nd core exercise.
  • Write an analyser for our current 2nd core exercise, leap.
  • Write an analyser for a different exercise.

What would be the implications of writing an analyser for leap rather than two-fer?

It would make sense to write an analyser for an early exercise so we get a lot of feedback.

If none of the current early exercises are good candidates, we can add a new one.

@petertseng
Copy link
Member Author

Based on this discussion so far, it currently seems that I should stand by my recommendation; I continue to recommend that Haskell and the other tracks using optional values should not use two-fer as their second core exercise.

I will not stand in the way if we collectively decide to ignore this recommendation, though there had better be a good reason to ignore it. Ultimately I can't claim to be an authority on curriculum design and I'm not a mentor on any track, so I can't speak to how such decisions affect the mentor and student experiences.

As stated in https://github.com/exercism/automated-mentoring-support , the analysers aim to make mentors' lives easier. Under this consideration there is no reason it has to be two-fer. If it doesn't exist (if the decision becomes not to add it), an analyser for it will not make lives easier.

@petertseng
Copy link
Member Author

The question I wanted to discuss (what would two-fer look like) has been answered. Any desired actions should have their own issue.

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

No branches or pull requests

3 participants