I am pretty new to Ramda and functional programming and trying to rewrite a script with Ramda but am unsure of how to handle errors with Ramda in a clean way. This is what I have, does anyone have any pointers with how to rewrite this in a functional way using Ramda?
const targetColumnIndexes = targetColumns.map(h => {
if (header.indexOf(h) == -1) {
throw new Error(`Target Column Name not found in CSV header column: ${h}`)
}
return header.indexOf(h)
})
For reference, these are the values of header
and targetColumns
const header = [ 'CurrencyCode', 'Name', 'CountryCode' ]
const targetColumns = [ 'CurrencyCode', 'Name' ]
So I need to:
-1
Ramda is a library of functions designed to make functional programming in JavaScript easy and powerful without making it any less like JavaScript.
compose FunctionPerforms right-to-left function composition. The rightmost function may have any arity; the remaining functions must be unary.
However, it might add quite a significant overhead. AWS Lambda error handling can be done in different ways, like utilizing wrappers. On the other hand, AWS Step Functions have proved to be incredibly beneficial for building a serverless application that’ll deal with retries and errors appropriately, making Step Functions an effective solution.
A Lambda function will generate errors randomly when found within the application. If CloudWatch Logs detect the word ERROR within the function’s logs, it’ll provide the processor function with an event for processing. The data has details about the log event when it’s decoded.
When failure occurs, and it will occur at some point, you’ll most likely notice Lambda retries based on these behaviors: Stream-based events – if the current events are solely DynamoDB streams and AWS Kinesis Data Streams.
Raised unhandled exception — can happen because of a programming bug, failure of an external API, or if you’ve received an invalid input. Timeout — ‘Task timed out after X seconds’ message appears when Lambda closes violently because it ran longer than the pre-configured timeout duration.
As customcommander says, there is a good reason that this style of throwing exceptions is not made easy by functional programming: it's much harder to reason about.
"What does you function return?"
"A number."
"Always?"
"Yes, ... well unless it throws an exception."
"Then what does it return?"
"Well it doesn't."
"So it returns a number or nothing at all?"
"I guess so."
"Hmmm."
One of the most common operations in functional programming is composing two functions. But that only works if the output of one function match the input of its successor. This is difficult if the first one might throw an exception.
To deal with this the FP world uses types that capture the notions of failure. You may have seen talk of the Maybe
type, which handles values that might be null
. Another common one is Either
(sometimes Result
), which has two subtypes, for the error case and the success one (respectively Left
and Right
for Either
or Error
and Ok
for Result
.) In these types, the first error found is captured and passed down the line to whoever needs it, while the success case continues to process. (There are also Validation
types that capture a list of errors.)
There are many implementations of these types. See the fantasy-land list for some suggestions.
Ramda used to have its own set of these types, but has backed away from maintaining it. Folktale and Sanctuary are the ones we often recommend for this. But even Ramda's old implementation should do. This version uses Folktale's data.either
as it's one I know better, but later versions of Folktale replace this with a Result
.
The following code block shows how I might use Either
s to handle this notion of failure, especially how we can use R.sequence
to convert an array of Eithers
into an Either
holding an array. If the input includes any Left
s, the output is just a Left
. If it's all Right
s, then the output is a Right
containing an array of their values. With this we can convert all our column names into Either
s that capture the value or the error, but then combine them into a single result.
The thing to note is that there are no exceptions thrown here. Our functions will compose properly. The notion of failure is encapsulated in the type.
const header = [ 'CurrencyCode', 'Name', 'CountryCode' ]
const getIndices = (header) => (targetColumns) =>
map((h, idx = header.indexOf(h)) => idx > -1
? Right(idx)
: Left(`Target Column Name not found in CSV header column: ${h}`)
)(targetColumns)
const getTargetIndices = getIndices(header)
// ----------
const goodIndices = getTargetIndices(['CurrencyCode', 'Name'])
console.log('============================================')
console.log(map(i => i.toString(), goodIndices)) //~> [Right(0), Right(1)]
console.log(map(i => i.isLeft, goodIndices)) //~> [false, false]
console.log(map(i => i.isRight, goodIndices)) //~> [true, true]
console.log(map(i => i.value, goodIndices)) //~> [0, 1]
console.log('--------------------------------------------')
const allGoods = sequence(of, goodIndices)
console.log(allGoods.toString()) //~> Right([0, 1])
console.log(allGoods.isLeft) //~> false
console.log(allGoods.isRight) //~> true
console.log(allGoods.value) //~> [0, 1]
console.log('============================================')
//----------
const badIndices = getTargetIndices(['CurrencyCode', 'Name', 'FooBar'])
console.log('============================================')
console.log(map(i => i.toString(), badIndices)) //~> [Right(0), Right(1), Left('Target Column Name not found in CSV header column: FooBar')
console.log(map(i => i.isLeft, badIndices)) //~> [false, false, true]
console.log(map(i => i.isRight, badIndices)) //~> [true, true, false]
console.log(map(i => i.value, badIndices)) //~> [0, 1, 'Target Column Name not found in CSV header column: FooBar']
console.log('--------------------------------------------')
const allBads = sequence(of, badIndices)
console.log(allBads.toString()) //~> Left('Target Column Name not found in CSV header column: FooBar')
console.log(allBads.isLeft) //~> true
console.log(allBads.isRight) //~> false
console.log(allBads.value) //~> 'Target Column Name not found in CSV header column: FooBar'
console.log('============================================')
.as-console-wrapper {height: 100% !important}
<script src="//bundle.run/[email protected]"></script>
<!--script src="//bundle.run/[email protected]"></script-->
<script src="//bundle.run/[email protected]"></script>
<script>
const {map, includes, sequence} = ramda
const Either = data_either;
const {Left, Right, of} = Either
</script>
The main point to me is that values such as goodIndices
and badIndices
are useful on their own. If we want to do more processing with them, we can simply map
over them. Note for instance that
map(n => n * n, Right(5)) //=> Right(25)
map(n => n * n, Left('oops')) //=> Left('oops'))
So our errors are left alone and our successes are processed further.
map(map(n => n + 1), badIndices)
//=> [Right(1), Right(2), Left('Target Column Name not found in CSV header column: FooBar')]
And this is what these types are all about.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With