Run this notebook

Use Livebook to open this notebook and explore new ideas.

It is easy to get started, on your machine or the cloud.

Click below to open and run it in your Livebook at .

(or change your Livebook location)

# 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 ```elixir Mix.install([ {:type_check, "~> 0.10.6"}, {:stream_data, "~> 0.5.0"} ]) ``` ## Refactor internal implementation to separate starting value and all subequent changes ```elixir 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) ```elixir ExUnit.start() defmodule MyValSpecTest do use ExUnit.Case, async: true use TypeCheck.ExUnit spectest(MyVal) end ``` ```elixir 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 ``` ```elixir 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 ``` ```elixir String.Chars.impl_for([]) ``` ```elixir 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() ``` ```elixir 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() ``` ```elixir 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() ``` ```elixir 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() ``` ```elixir 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() ``` ```elixir MyVal.new(88) |> MyVal.multiply(17) |> MyVal.add(1_000_000) |> MyVal.peek() |> MyVal.divide(100) |> MyVal.val() ``` ```elixir MyVal.new("a") ``` ```elixir %MyVal{start_val: -111, changes: [7, 9]} |> MyVal.val() ``` ```elixir %MyVal{changes: [{7, {:+, 8}}]} |> MyVal.val() ``` ```elixir %MyVal{changes: [{nil, {:+, 99}}]} |> MyVal.val() ``` ```elixir %MyVal{changes: [{7, {nil, 99}}]} |> MyVal.val() ``` ```elixir %MyVal{changes: [{7, {:+, nil}}]} |> MyVal.val() ``` ```elixir MyVal.new(-9) |> MyVal.add(9) |> MyVal.add("3") ``` ```elixir MyVal.new(-9) |> MyVal.add(9) |> MyVal.subtract("3") ``` ```elixir MyVal.new(88) |> MyVal.multiply(17) |> MyVal.add(1_000_000) |> MyVal.peek() |> MyVal.divide(100) |> MyVal.recalculate() ``` ```elixir 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 ```elixir 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 ```elixir MyVal.new(8) |> MyVal.apply_action({:+, 9}) |> MyVal.apply_action({:-, 19}) ``` ```elixir MyVal.new(8) |> MyVal.apply_actions([{:+, 9}, {:-, 19}]) ``` ## Property-based test of recalculate/1 vs. apply_actions/2 ```elixir 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() ```
See source

Have you already installed Livebook?

If you already installed Livebook, you can configure the default Livebook location where you want to open notebooks.
Livebook up Checking status We can't reach this Livebook (but we saved your preference anyway)
Run notebook

Not yet? Install Livebook in just a minute

Livebook is open source, free, and ready to run anywhere.

Run in the cloud

on select platforms

To run on Linux, Docker, embedded devices, or Elixir’s Mix, check our README.

PLATINUM SPONSORS
SPONSORS
Code navigation with go to definition of modules and functions Read More