lOlive
lOlive

Reputation: 233

Proper refactor of a Haskell code when a non-monadic value becomes a monadic value

Let's say I have this sample code:

h = 2
add = \x y -> (x + y)
addH = add h

main = return (fmap addH [1,2])

Running it evaluates to [3,4]


Now, let's say h is not set to "2", but to "Just 2".

Question, part 1:

What is the proper refactor, then, to still return [3,4] in the end?

Question, part 2:

Would a skilled Haskell developper prefer to change the return value to be [Just 3, Just 4]

For example with a refactor like this:

h = Just 2
add = \x y -> (x + y)
addH = liftM2(add) h . pure         --isn't there a better syntax for that?

main = return (fmap addH [1,2])

More generally, in an existing codebase, how to minimize the refactor impact when a function that used to return 'Num t => [t]' must now return 'Num t => [Maybe t]'?

Upvotes: 0

Views: 114

Answers (2)

MikaelF
MikaelF

Reputation: 3625

I would like to point out that in your original code, [] and Maybe are redundant, and that a closer inspection of the meaning of these two functors leads to an interesting discussion. [] (list) represents a success or a failure in the same sense that Maybe does, the main difference being that [] can represent 0 or more values, whereas Maybe is limited to 0 or 1 value.

This leads to another consideration about the meaning of the Just in Just 2. What does it mean for 2 to be wrapped in a Just, and what would a Nothing mean? To take this one step further, here are five different results that you could opt for, each having a different meaning:

  1. Just [3, 4]: There was one monolithic computation that dealt with multiple values, and it succeeded.
  2. Nothing: There could have been a monolithic value, but there is none.
  3. [3, 4]: There was a batch of computations, and these are the values that came out.
  4. []: There could have been multiple values, but there are none.
  5. [Just 3, Just 4]: There was a batch of computations, where each computation could have failed, and all of them succeeded.

Case 1: If h represents a monolithic value, and is the unique point of failure in this computation chain, then options 1 and 2 seem like a reasonable choice. The following code accomplishes this:

import Control.Applicative (LiftA2)
h = Just 2
add = (+)
addH = LiftA2 add h . pure
-- Returns Just [3,4] or Nothing
mainFunc = traverse addH [1,2]

Case 2: If add represents a function that can fail depending on its argument (for example, think of the function hDivBy, which would fail on every 0), then options 3 and 4 seem like a good choice.

import Data.Maybe (mapMaybe)
hDivBy = \x -> (`div` x) <$> h
--Returns [1]
mainFunc = mapMaybe hDivBy [0, 2]

Case 3: If you want to keep track of element indexing, then option 5 will give you just that:

--Returns [Nothing, Just 2]
mainFunc = fmap hDivBy [0,2]

Notice that this code keeps everything in the Applicative realm, making it more general, amongst other advantages.

Upvotes: 1

Pawan Kumar
Pawan Kumar

Reputation: 1533

To acheive Fyodor Soikin result you can try the below one.

h = Just 2
add = \x y -> x + y
addH arr = pure fmap <*> (add <$> h) <*> pure arr

main = return $ addH [1, 2]

Upvotes: 0

Related Questions