MyVal (version 5: Refactor MyVal from :history to :start_val and :changes)
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"}
])
Refactor internal implementation to separate starting value and all subequent changes
defmodule MyVal do
use TypeCheck
@enforce_keys [:start_val, :changes]
defstruct [:start_val, changes: []]
@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()}
@spec! new(number()) :: %MyVal{}
def new(val), do: %MyVal{start_val: val, changes: []}
@spec! apply_actions(t(), actions()) :: t()
def apply_actions(%MyVal{start_val: sv, changes: changes} = mv, actions) do
actions
|> Enum.reduce(mv, fn action, acc -> apply_action(acc, action) end)
end
@spec! apply_action(t(), action()) :: t()
def apply_action(%MyVal{start_val: sv, changes: changes} = mv, {:+, operand}) do
add(mv, operand)
end
def apply_action(%MyVal{start_val: sv, changes: changes} = mv, {:-, operand}) do
subtract(mv, operand)
end
def apply_action(%MyVal{start_val: sv, changes: changes} = mv, {:*, operand}) do
multiply(mv, operand)
end
def apply_action(%MyVal{start_val: sv, changes: changes} = 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()
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()
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()
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()
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()
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()
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()}
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()
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())
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())
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()
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
Use TypeCheck’s spectest(MyVal)
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}}]}
|> MyVal.val()
%MyVal{changes: [{7, {nil, 99}}]}
|> MyVal.val()
%MyVal{changes: [{7, {:+, nil}}]}
|> 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()
Modify TypeCheck generator to generate actions
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()
Example-based tests of apply_action/2 & apply_actions/2
MyVal.new(8)
|> MyVal.apply_action({:+, 9})
|> MyVal.apply_action({:-, 19})
MyVal.new(8)
|> MyVal.apply_actions([{:+, 9}, {:-, 19}])
Property-based test of recalculate/1 vs. 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 "apply_action vs apply_actions", %{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)
|> IO.inspect(label: "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)
|> 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()