exercism/elixir/rpn-calculator
Danil Negrienko 7ae168f5ea rpn_calculator v2 2024-03-07 02:05:21 -05:00
..
.exercism rpn_calculator 2024-03-07 01:40:53 -05:00
lib rpn_calculator v2 2024-03-07 02:05:21 -05:00
test rpn_calculator 2024-03-07 01:40:53 -05:00
.formatter.exs rpn_calculator 2024-03-07 01:40:53 -05:00
.gitignore rpn_calculator 2024-03-07 01:40:53 -05:00
HELP.md rpn_calculator 2024-03-07 01:40:53 -05:00
HINTS.md rpn_calculator 2024-03-07 01:40:53 -05:00
README.md rpn_calculator 2024-03-07 01:40:53 -05:00
mix.exs rpn_calculator 2024-03-07 01:40:53 -05:00

README.md

RPN Calculator

Welcome to RPN Calculator on Exercism's Elixir Track. If you need help running the tests or submitting your code, check out HELP.md. If you get stuck on the exercise, check out HINTS.md, but try and solve it without using those first :)

Introduction

Errors

Errors happen. In Elixir, while people often say to "let it crash", there are times when we need to rescue the function call to a known good state to fulfill a software contract. In some languages, errors are used as a method of control flow, but in Elixir, this pattern is discouraged. We can often recognize functions that may raise an error just by their name: functions that raise errors are to have ! at the end of their name. This is in comparison with functions that return {:ok, value} or :error. Look at these library examples:

Map.fetch(%{a: 1}, :b)
# => :error
Map.fetch!(%{a: 1}, :b)
# => raises KeyError

Try/Rescue

Elixir provides a construct for rescuing from errors using try .. rescue

[]: # (elixir-formatter-disable-next-block)

try do                             #1
  raise RuntimeError, "error"      #2
rescue
  e in RuntimeError -> :error      #3
end

Let's examine this construct:

  • Line 1, the block is declared with try.
  • Line 2, the function call which may error is placed here, in this case we are calling raise/2.
  • Line 3, in the rescue section, we pattern match on the Module name of the error raised
    • on the left side of ->:
      • e is matched to the error struct.
      • in is a keyword.
      • RuntimeError is the error that we want to rescue.
        • If we wanted to rescue from all errors, we could use _ instead of the module name or omit the in keyword entirely.
    • on the right side:
      • the instructions to be executed if the error matches.

Error structs

Errors (sometimes also called "exceptions") that you rescue this way are structs. Rescuing errors in Elixir is done very rarely. Usually the rescued error is logged or sent to an external monitoring service, and then reraised. This means we usually don't care about the internal structure of the specific error struct.

In the Exceptions concept you will learn more about error structs, including how to define your own custom error.

Instructions

While working at Instruments of Texas, you are tasked to work on an experimental Reverse Polish Notation [RPN] calculator written in Elixir. Your team is having a problem with some operations raising errors and crashing the process. You have been tasked to write a function which wraps the operation function so that the errors can be handled more elegantly with idiomatic Elixir code.

1. Warn the team

Implement the function calculate!/2 to call the operation function with the stack as the only argument. The operation function is defined elsewhere, but you know that it can either complete successfully or raise an error.

stack = []
operation = fn _ -> :ok end
RPNCalculator.calculate!(stack, operation)
# => :ok

stack = []
operation = fn _ -> raise ArgumentError, "An error occurred" end
RPNCalculator.calculate!(stack, operation)
# => ** (ArgumentError) An error occurred

Function names that end in ! are a warning to programmers that this function may raise an error

2. Wrap the error

When doing more research you notice that many functions use atoms and tuples to indicate their success/failure. Implement calculate/2 using this strategy.

stack = []
operation = fn _ -> "operation completed" end
RPNCalculator.calculate(stack, operation)
# => {:ok, "operation completed"}

stack = []
operation = fn _ -> raise ArgumentError, "An error occurred" end
RPNCalculator.calculate(stack, operation)
# => :error

3. Pass on the message

Some of the errors contain important information that your coworkers need to have to ensure the correct operation of the system. Implement calculate_verbose/2 to pass on the error message. The error is a struct that has a :message field.

stack = []
operation = fn _ -> "operation completed" end
RPNCalculator.calculate_verbose(stack, operation)
# => {:ok, "operation completed"}

stack = []
operation = fn _ -> raise ArgumentError, "An error occurred" end
RPNCalculator.calculate_verbose(stack, operation)
# => {:error, "An error occurred"}

Source

Created by

  • @neenjaw

Contributed to by

  • @angelikatyborska
  • @cjmaxik