MyVal (version 6: handle failing{:/, 0} case)
Documentation on @types, @specs, StreamData, & TypeCheck
Types & Specs:
- https://hexdocs.pm/elixir/typespecs.html
- https://elixir-lang.org/getting-started/typespecs-and-behaviours.html
- https://hexdocs.pm/elixir/1.13.2/typespecs.html
StreamData:
- https://hexdocs.pm/stream_data/StreamData.html
- https://elixirschool.com/en/lessons/libraries/stream-data/
- https://github.com/whatyouhide/stream_data
TypeCheck:
- https://github.com/Qqwy/elixir-type_check
- https://hexdocs.pm/type_check/readme.html
- https://hexdocs.pm/type_check/type-checking-and-spec-testing-with-typecheck.html
Mix.install([
{:type_check, "~> 0.10.6"},
{:stream_data, "~> 0.5.0"}
])
Add :valid?; test validity before running calculations; when invalid, record :errors & return error tuples
defmodule MyVal do
use TypeCheck
@enforce_keys [:start_val, :changes, :valid?]
defstruct [:start_val, changes: [], errors: [], valid?: true]
@type! start_val :: number()
@type! my_val_ops :: :+ | :- | :* | :/
@type! action :: {my_val_ops(), number()}
@type! actions :: list(action())
@type! change :: {number(), action()}
@type! changes :: list(change())
# @type! first_calc :: {nil, nil}
# @type! last_calc :: first_calc() | ordinary_calc()
# @type! operation :: {:+ | :- | :* | :/, number()}
# @type! first_entry :: {number(), {nil, nil}}
# @type! my_val_entry :: {number(), operation()}
# @type! my_val_entry :: {number(), {nil, nil}} | {number(), {:+ | :- | :* | :/, number()}}
# @type! my_val_initial_entry :: {number(), {nil, nil}}
# @type! my_val_subsequent_entry :: {number(), {my_val_ops(), number()}}
# @type! my_val_entry :: my_val_initial_entry() | my_val_subsequent_entry()
# @type! ordered_entry_list :: nonempty_list(my_val_initial_entry()) | nonempty_list(my_val_entry())
@type! ordered_entry_list :: nonempty_list()
@type! t :: %MyVal{start_val: start_val(), changes: changes(), valid?: boolean()}
@spec! new(number()) :: %MyVal{}
def new(val), do: %MyVal{start_val: val, changes: [], valid?: true}
@spec! apply_actions(t(), actions()) :: t() | {:error, t()}
def apply_actions(%MyVal{valid?: false} = myval, _actions) do
myval |> wrap_in_already_invalid_error_tuple
end
def apply_actions(%MyVal{start_val: sv, changes: _changes} = myval, actions) do
case any_invalid_action?(actions) do
true ->
myval |> invalidate_and_wrap_in_divide_by_zero_error_tuple()
false ->
actions
|> Enum.reduce(myval, fn action, acc -> apply_action(acc, action) end)
end
end
defp any_invalid_action?(actions) do
Enum.any?(actions, fn {op, operand} -> op == :/ && operand == 0 end)
end
defp invalidate_due_to_attempt_to_divide_by_zero(myval) do
errors =
["Attempted to apply an invalid action to this MyVal instance"] ++ Map.get(myval, :errors)
%{myval | valid?: false, errors: errors}
end
defp invalidate_and_wrap_in_divide_by_zero_error_tuple(myval) do
myval
|> invalidate_due_to_attempt_to_divide_by_zero()
|> (fn mv -> {:error, mv} end).()
end
defp wrap_in_already_invalid_error_tuple(myval) do
errors =
["Attempted to apply_actions to an invalid MyVal instance"] ++ Map.get(myval, :errors)
{:error, %{myval | errors: errors}}
end
@spec! apply_action(t(), action()) :: t() | {:error, t()}
def apply_action(%MyVal{valid?: false} = mv, {_op, _operand}) do
mv |> wrap_in_already_invalid_error_tuple
end
def apply_action(%MyVal{valid?: true} = mv, {:+, operand}) do
add(mv, operand)
end
def apply_action(%MyVal{valid?: true} = mv, {:-, operand}) do
subtract(mv, operand)
end
def apply_action(%MyVal{valid?: true} = mv, {:*, operand}) do
multiply(mv, operand)
end
def apply_action(%MyVal{valid?: true} = mv, {:/, 0}) do
# %{mv | valid?: false, errors: ["Attempted to divide by zero"]}
# {:error, %{mv | valid?: false, changes: [{:invalid, {:/, 0}}], errors: ["Attempted to divide by zero"]}}
mv |> invalidate_and_wrap_in_divide_by_zero_error_tuple()
end
def apply_action(%MyVal{valid?: true} = mv, {:/, operand}) do
divide(mv, operand)
end
# initial_value = mv |> val()
# actions
# |> Enum.reduce(, fn {_val, {op, operand}}, acc ->
# apply(Kernel, op, [acc, operand])
# end)
# %{mv | changes: new_changes}
@spec! val(t()) :: number() | :error
def val(%MyVal{valid?: false}), do: :error
def val(%MyVal{start_val: sv, changes: []}) when is_number(sv), do: sv
def val(%MyVal{changes: [{hd_val, {_hd_op, _hd_operand}} | _tl]}), do: hd_val
@spec! peek(t()) :: t() | :error
def peek(%MyVal{valid?: false}), do: :error
def peek(%MyVal{changes: [{hd_val, {_hd_op, _hd_operand}} | _tl]} = my_val) do
IO.inspect(hd_val, label: "current value of MyVal instance")
my_val
end
def peek(%MyVal{start_val: sv, changes: []} = my_val) do
IO.inspect(sv, label: "current value of MyVal instance")
my_val
end
@spec! add(t(), number()) :: t() | {:error, t()}
def add(%MyVal{valid?: false} = mv, _operand), do: mv |> wrap_in_already_invalid_error_tuple()
def add(%MyVal{changes: changes} = my_val, new_val) when is_list(changes) do
cur_val = my_val |> val()
%{my_val | changes: [{cur_val + new_val, {:+, new_val}} | changes]}
end
@spec! subtract(t(), number()) :: t() | {:error, t()}
def subtract(%MyVal{valid?: false} = mv, _operand),
do: mv |> wrap_in_already_invalid_error_tuple()
def subtract(%MyVal{changes: changes} = my_val, new_val) when is_list(changes) do
cur_val = my_val |> val()
%{my_val | changes: [{cur_val - new_val, {:-, new_val}} | changes]}
end
@spec! multiply(t(), number()) :: t() | {:error, t()}
def multiply(%MyVal{valid?: false} = mv, _operand),
do: mv |> wrap_in_already_invalid_error_tuple()
def multiply(%MyVal{changes: changes} = my_val, new_val) when is_list(changes) do
cur_val = my_val |> val()
%{my_val | changes: [{cur_val * new_val, {:*, new_val}} | changes]}
end
@spec! divide(t(), number()) :: t() | {:error, t()}
def divide(%MyVal{valid?: false} = mv, _operand),
do: mv |> wrap_in_already_invalid_error_tuple()
def divide(my_val, 0), do: my_val |> invalidate_and_wrap_in_divide_by_zero_error_tuple()
def divide(%MyVal{changes: changes} = my_val, new_val) when is_list(changes) do
cur_val = my_val |> val()
%{my_val | changes: [{cur_val / new_val, {:/, new_val}} | changes]}
end
@spec! view(t()) :: %{val: number(), my_val: t()} | {:error, t()}
def view(%MyVal{valid?: false} = mv), do: mv |> wrap_in_already_invalid_error_tuple()
def view(%MyVal{changes: _changes} = my_val) do
%{val: MyVal.val(my_val), my_val: my_val}
end
@spec! ordered_history(t()) :: ordered_entry_list() | {:error, t()}
def ordered_history(%MyVal{valid?: false} = mv), do: mv |> wrap_in_already_invalid_error_tuple()
def ordered_history(%MyVal{start_val: sv, changes: changes}) do
changes
|> Enum.reverse()
|> List.insert_at(0, {sv, nil})
end
@spec! show_history(t()) :: list(String.t()) | {:error, t()}
def show_history(%MyVal{valid?: false} = mv), do: mv |> wrap_in_already_invalid_error_tuple()
def show_history(%MyVal{start_val: sv, changes: changes}) do
changes
|> Enum.reverse()
|> Enum.reduce(
["#{sv}"],
&stringify_next_line/2
)
|> Enum.reverse()
end
@spec! show_history2(t()) :: list(String.t()) | {:error, t()}
def show_history2(%MyVal{valid?: false} = mv), do: mv |> wrap_in_already_invalid_error_tuple()
def show_history2(%MyVal{start_val: sv, changes: changes}) do
changes
|> Enum.reverse()
|> Enum.reduce(
["#{sv}"],
fn {val, {op, operand}}, acc -> ["#{op} #{operand} = #{val}" | acc] end
)
|> Enum.reverse()
end
@spec! recalculate(t()) :: number() | {:error, t()}
def recalculate(%MyVal{valid?: false} = mv), do: mv |> wrap_in_already_invalid_error_tuple()
def recalculate(%MyVal{start_val: sv, changes: changes} = my_val) do
changes
|> Enum.reverse()
|> Enum.reduce(sv, fn {_val, {op, operand}}, acc ->
apply(Kernel, op, [acc, operand])
end)
end
defp stringify_next_line({_val, {nil, nil}} = entry, acc) do
[stringify_entry(entry) | acc]
end
defp stringify_next_line({_val, {_op, _operand}} = entry, acc) do
[stringify_entry(entry) | acc]
end
defp stringify_entry({val, {nil, nil}}) do
"#{val}"
end
defp stringify_entry({val, {op, operand}}) do
"#{op} #{operand} = #{val}"
end
end
ExUnit.start()
defmodule MyValSpecTest do
use ExUnit.Case, async: true
use TypeCheck.ExUnit
spectest(MyVal)
end
defimpl Inspect, for: MyVal do
def inspect(my_val) do
# %MyVal{my_value: #{my_val.value}, history: #{my_val.history}}
"""
#{my_val |> MyVal.val()}
"""
end
end
defimpl String.Chars, for: MyVal do
def to_string(my_val) do
# %MyVal{my_value: #{my_val.value}, history: #{my_val.history}}
"""
#{my_val |> MyVal.val()}
"""
end
end
String.Chars.impl_for([])
MyVal.new(-8)
|> MyVal.add(11)
|> MyVal.add(4)
|> MyVal.add(8)
|> MyVal.view()
# |> (fn my_val -> %{val: MyVal.val(my_val), my_val: my_val} end).()
# |> inspect()
MyVal.new(-8)
|> MyVal.add(11)
|> MyVal.add(4)
|> MyVal.subtract(8)
|> MyVal.add(11)
|> MyVal.multiply(10)
|> MyVal.subtract(10)
|> MyVal.divide(10)
|> MyVal.view()
MyVal.new(-8)
|> MyVal.add(11)
|> MyVal.add(4)
|> MyVal.subtract(8)
|> MyVal.add(11)
|> MyVal.multiply(10)
|> MyVal.subtract(10)
|> MyVal.divide(10)
|> MyVal.show_history()
MyVal.new(-8)
|> MyVal.add(11)
|> MyVal.add(4)
|> MyVal.subtract(8)
|> MyVal.add(11)
|> MyVal.multiply(10)
|> MyVal.subtract(10)
|> MyVal.divide(10)
|> MyVal.show_history2()
MyVal.new(-8)
|> MyVal.add(11)
|> MyVal.add(4)
|> MyVal.subtract(8)
|> MyVal.add(11)
|> MyVal.multiply(10)
|> MyVal.subtract(10)
|> MyVal.divide(10)
|> MyVal.ordered_history()
MyVal.new(88)
|> MyVal.multiply(17)
|> MyVal.add(1_000_000)
|> MyVal.peek()
|> MyVal.divide(100)
|> MyVal.val()
# MyVal.new("a")
# %MyVal{start_val: -111, changes: [7, 9]} |> MyVal.val()
# %MyVal{changes: [{7, {:+, 8}}]} |> MyVal.val()
%MyVal{changes: [{nil, {:+, 99}}], valid?: true, errors: []} |> MyVal.val()
# %MyVal{changes: [{7, {nil, 99}}], valid?: true} |> MyVal.val()
# %MyVal{changes: [{7, {:+, nil}}], valid?: true} |> MyVal.val()
MyVal.new(-9)
|> MyVal.add(9)
|> MyVal.add("3")
MyVal.new(-9)
|> MyVal.add(9)
|> MyVal.subtract("3")
MyVal.new(88)
|> MyVal.multiply(17)
|> MyVal.add(1_000_000)
|> MyVal.peek()
|> MyVal.divide(100)
|> MyVal.recalculate()
defmodule MyValTest do
use ExUnit.Case
require MyVal
setup do
steps = [
{:add, 1},
{:divide, 10},
{:multiply, 2},
{:subtract, 10}
]
%{starting_value: 99, steps: steps}
end
test "silly test" do
assert 3 == 2 + 1
end
test "recalculate and val return the same value", %{starting_value: sv, steps: steps} do
final_my_value =
steps
|> Enum.reduce(
MyVal.new(sv),
fn {op, operand}, acc -> apply(MyVal, op, [acc, operand]) end
)
assert MyVal.recalculate(final_my_value) == MyVal.val(final_my_value)
end
end
ExUnit.run()
defmodule MyValTest2 do
use ExUnit.Case
use TypeCheck
require MyVal
import TypeCheck.Type.StreamData
setup do
myval_generator = TypeCheck.Type.build({MyVal.actions()}) |> to_gen()
%{myval_gen: myval_generator}
end
test "generation", %{myval_gen: myval_gen} do
StreamData.list_of(myval_gen, length: 50) |> Enum.take(1) |> IO.inspect(label: "sample data")
end
end
ExUnit.run()
MyVal.new(8)
|> MyVal.apply_action({:+, 9})
|> MyVal.apply_action({:-, 19})
MyVal.new(8)
|> MyVal.apply_actions([{:+, 9}, {:-, 19}])
Example-based tests for division by zero; prevent division by zero in property-based tests; generate actions & build valid MyVals via apply_actions/2
defmodule MyValTest3 do
use ExUnit.Case
use TypeCheck
require MyVal
import TypeCheck.Type.StreamData
setup do
actions_generator = TypeCheck.Type.build(MyVal.action()) |> to_gen()
# This didn't work because the generated instances weren't valid
# I'll generate valid instances from more primative building blocks
# myval_generator = TypeCheck.Type.build(MyVal.t()) |> to_gen()
%{actions_gen: actions_generator}
end
test "attempting to divide by 0 returns :error tuple" do
result =
MyVal.new(11)
|> MyVal.divide(0)
assert {:error, %MyVal{valid?: false}} = result
end
test "attempting to apply_action {:/, 0} returns :error tuple" do
result =
MyVal.new(11)
|> MyVal.apply_action({:/, 0})
assert {:error, %MyVal{valid?: false}} = result
end
test "attempting to apply_actions when one action is {:/, 0} returns :error tuple" do
result =
MyVal.new(11)
|> MyVal.apply_actions([{:+, 7}, {:*, 9}, {:/, 0}, {:-, -11}])
assert {:error, %MyVal{valid?: false}} = result
end
def remove_divide_by_zero_actions(list_of_actions) do
list_of_actions
|> Enum.reject(fn {op, operand} -> op == :/ && operand == 0 end)
end
test "apply_action vs apply_actions (when no division by zero)", %{actions_gen: actions_gen} do
max_action_length = 20
num_tests = 10
start_vals =
StreamData.integer(-500..10_000) |> Enum.take(num_tests) |> IO.inspect(label: "start_vals")
myvals_actions =
StreamData.list_of(actions_gen, max_length: max_action_length)
|> Enum.take(num_tests)
# |> Enum.map(fn list -> Enum.reject(list, fn {op, operand} -> op == :/ && operand == 0 end) end)
|> Enum.map(&remove_divide_by_zero_actions/1)
|> IO.inspect(label: "myvals_actions")
myvals =
start_vals
|> Enum.map(&MyVal.new/1)
|> Enum.zip(myvals_actions)
|> Enum.map(fn {mv, mvas} -> MyVal.apply_actions(mv, mvas) end)
|> IO.inspect(label: "myvals")
actions =
StreamData.list_of(actions_gen, max_length: max_action_length)
|> Enum.take(num_tests)
|> Enum.map(&remove_divide_by_zero_actions/1)
|> IO.inspect(label: "actions")
for idx <- 0..(num_tests - 1) do
actions_applied =
MyVal.apply_actions(myvals |> Enum.at(idx), actions |> Enum.at(idx))
|> IO.inspect(label: "actions_applied")
assert actions_applied |> MyVal.val() == MyVal.recalculate(actions_applied)
end
end
end
ExUnit.run()