miciek / grokkingfp-examples

All examples and exercises from the Grokking Functional Programming book
https://www.manning.com/books/grokking-functional-programming?utm_source=michal&utm_medium=affiliate&utm_campaign=book_khan_grokking_6_26_19&a_aid=michal&a_bid=7c041142
134 stars 51 forks source link

Book says exception thrown for IO but it is not #152

Closed gitgithan closed 1 year ago

gitgithan commented 1 year ago

On page 293 Chapter 8 Lazy and Eager evaluation

val program = IO.pure(2022).orElse(
 IO.pure(throw new Exception())
)
→ Exception thrown

Book says above that exception is thrown. However when i run in repl, i get a lazy output?

scala> val program = IO.pure(2022).orElse(
     |  IO.pure(throw new Exception())
     | )
val program: cats.effect.IO[Int] = IO(...)
miciek commented 1 year ago

Thank you @gitgithan! That's a great catch and one of two issues with the code found so far by readers!

Here's the PR that fixes it: #153.

(quoting from the PR) Here are the correct examples:

IO.delay(throw new Exception).orElse(IO.pure(2022)) // lazy, doesn't throw, returns 2022 when run
IO.pure(throw new Exception).orElse(IO.pure(2022)) // eager, throws before creating a value

I hope you are still enjoying the book despite this problem! This and few others will be fixed in the livebook version soon and in the paper version when it goes to reprint :)

Let me know if you have any questions or problems! Thank you again!

gitgithan commented 1 year ago

Thanks for the fix! Do you mean any IO placed inside orElse will have to wait for unsafeRunSync() no matter if its pure or delay?

The book is going great, refactoring step by step is really effective in getting readers to think about each step without overwhelming their attention. I like how requirements are encoded as functions and composed together and the style of writing with focus on maintainability rather than DRY.

For FP, I'm concerned about how difficult it is to refactor given that there's so much upfront design of types (maybe the counterargument is having clear types makes it easier?). Also I'm wondering how easy it is to debug things while refactoring since the compiler won't even work if the types don't match across function boundaries during refactoring.

I wish there was more examples of the conversion under the hood from for comprehensions to flatmap/map because once equals appeared in for comprehension, it got confusing whether that line is its own generator of part of a multiple line function in scope of scheduledMeetings.map (I depended on chatgpt to convert this for-comprehension to both flatmap and map versions, then realized the flatmap version had to wrap IO.pure(meetings.headOption) instead of just meetings.headOption, so this reminded me given one enumerator, it has to be map because the last enumerators is map).

      for {
        existingMeetings <- scheduledMeetings(person1, person2)
        meetings          = possibleMeetings(existingMeetings, 8, 16, lengthHours)
      } yield meetings.headOption

The only other place i felt uncomfortable was page 190 saying " If both conditions are met, we know we can safely call rawShow.substring and wrap the value in Some"

def extractYearStart(rawShow: String): Option[Int] = {
 val bracketOpen = rawShow.indexOf('(')
 val dash = rawShow.indexOf('-')
 if (bracketOpen != -1 && dash > bracketOpen + 1)
 Some(rawShow.substring(bracketOpen + 1, dash))
 else None
}

It is uncomfortable because it sounds overconfident. The coder has to now be extremely sure/cautious that the if conditions are truly exhaustive enough that the Some-wrapped value is definitely going to produce something meaningful with no errors.

miciek commented 1 year ago

Thank you @gitgithan. That's definitely a problem with my example!

Thanks for the fix! Do you mean any IO placed inside orElse will have to wait for unsafeRunSync() no matter if its pure or delay?

Yes, this is an additional optimization/safety net in the library itself but I wouldn't use it. The main idea is to use IO.pure only for pure immutable values and IO.delay to anything that can have side effects (like throwing an exception or talking to an API). Then you are always on the safe side :)

The book is going great, refactoring step by step is really effective in getting readers to think about each step without overwhelming their attention. I like how requirements are encoded as functions and composed together and the style of writing with focus on maintainability rather than DRY.

Wow, that's great to see because that's exactly the whole point of the book. 🙌

For FP, I'm concerned about how difficult it is to refactor given that there's so much upfront design of types (maybe the counterargument is having clear types makes it easier?). Also I'm wondering how easy it is to debug things while refactoring since the compiler won't even work if the types don't match across function boundaries during refactoring.

Please continue reading if you are concerned about types and refactorings. Part 3 contains a real example of an application written in this style. We are also showing some refactorings along the way.

I wish there was more examples of the conversion under the hood from for comprehensions to flatmap/map because once equals appeared in for comprehension, it got confusing whether that line is its own generator of part of a multiple line function in scope of scheduledMeetings.map (I depended on chatgpt to convert this for-comprehension to both flatmap and map versions, then realized the flatmap version had to wrap IO.pure(meetings.headOption) instead of just meetings.headOption, so this reminded me given one enumerator, it has to be map because the last enumerators is map).

Nice! One other thing I can suggest is using IntelliJ to convert them for you. Alt-Enter while on for and then Desugar for comprehension.

The only other place i felt uncomfortable was page 190 saying " If both conditions are met, we know we can safely call rawShow.substring and wrap the value in Some"

It is uncomfortable because it sounds overconfident. The coder has to now be extremely sure/cautious that the if conditions are truly exhaustive enough that the Some-wrapped value is definitely going to produce something meaningful with no errors.

Yes, I agree. Maybe I should have stated it a bit softer. However, I hope that it still helped to understand the main takeaway :)