0

I am trying to do this for learning. But I can't get it to work (big surprise since you're reading this :) )

The algo:

  • Have a mixed list of valid/invalid objects as input
  • Extract the desired property from each object (or null)
  • Transform to a list of Options
  • Reduce the list of Options by using a function that operates on Aggregate types.

I couldn't get the types to work by elegantly lifting sumAggregates so I tried to do it with pipe, ap. But I would like to see how we would properly lift sumAggregates to be used in the reduce.

Please note that my goal isn't to get the correct result in a different way, but to learn why my implementation of this one fails.

  type Actionable = {
    action?: string
  }
  type Aggregate = {
    allowed: number,
    blocked: number
  }

  const emptyAggregate: Aggregate = {
    allowed: 0,
    blocked: 0
  }

  const list: Actionable[] = [ { action: 'block'}, { }, { action: 'block'}, { }, { action: 'allow'}]

  const extractAction = (a: Actionable) => a.action
  const stringToAggregator = (str: string): Aggregate => {
    return {
      allowed: str === 'allow' ? 1 : 0,
      blocked: str === 'block' ? 1 : 0,
    }
  }
  const sumAggregates = (a: Aggregate) => (b: Aggregate): Aggregate => {
    return {
      allowed: a.allowed + b.allowed,
      blocked: b.blocked + b.blocked,
    }
  }

  const totals: O.Option<Aggregate> = pipe(
    list,
    A.map(extractAction),
    A.map(O.fromNullable),
    A.map(O.map(stringToAggregator)),
    A.reduce(
      O.some(emptyAggregate),
      (a: O.Option<Aggregate>, b: O.Option<Aggregate>) => {
        return pipe(O.of(sumAggregates), O.ap(a), O.ap(b))
      }
    )
  )

Returns None instead of some({allowed: 1, blocked: 2})

Derrick Beining
  • 876
  • 8
  • 15
rollingBalls
  • 1,808
  • 1
  • 14
  • 25

1 Answers1

2

You end up with None because one of the elements in the list in the reduce is a None, and you pass that None to O.ap in the reducer.

Take a look at the definition of ap for Option here. The lifted computation does not get called when provided a None and the result of the ap becomes None as a result. So what would you expect the result to be for the following most basic computation?

import * as O from 'fp-ts/Option'
import {identity, pipe} from 'fp-ts/function'

const result: O.Option<number> = pipe(
  O.some(identity),
  O.ap(O.none),
)

It would be None right? That is the effect that Option's apply/applicative instance is known for. So in your reduce function, once a None is encountered and passed to ap, the accumulator ends up in the None case, and no further applications of sumAggregates can be performed.

Derrick Beining
  • 876
  • 8
  • 15
  • Thank you very much! Is there a way to replace the line `return pipe(O.of(sumAggregates), O.ap(a), O.ap(b))` with something else that works correctly (so, keeping the approach effectively the same, except for that function of the reducer), or is it impossible and a different algorithm must be used instead? – rollingBalls Feb 04 '22 at 15:07
  • 1
    @rollingBalls I believe something like this would work: `return pipe(O.of(sumAggregates), O.ap(a), O.ap(pipe(b, O.alt(() => O.some(emptyAggregate)))))` – Derrick Beining Feb 04 '22 at 17:11
  • Thank you! The usage `alt` is illuminating to me. There's probably some minor tweak needed somewhere else because it now returns `{ _tag: 'Some', value: { allowed: 1, blocked: 0 } }` (while the value before the reducer is `[{"_tag": "Some", "value": {"allowed": 0, "blocked": 1}}, {"_tag": "None"}, {"_tag": "Some", "value": {"allowed": 0, "blocked": 1}}, {"_tag": "None"}, {"_tag": "Some", "value": {"allowed": 1, "blocked": 0}}]`). But that doesn't matter, I understand now. Thanks again!! – rollingBalls Feb 04 '22 at 18:59