From d3d65ffd5afc6d4b2ec62aae413f530cf4a244c1 Mon Sep 17 00:00:00 2001 From: Chris Keele Date: Fri, 19 Apr 2024 02:40:07 -0500 Subject: [PATCH] Try module patching helpers --- README.md | 2 +- lib/livebook.ex | 9 ++ lib/livebook/helpers.ex | 18 ++++ lib/livebook/module.ex | 91 ++++++++++++++++ lib/livebooks.ex | 3 - livebooks/surreal.livemd | 228 ++++++--------------------------------- mix.exs | 37 +++++-- mix.lock | 2 + 8 files changed, 184 insertions(+), 206 deletions(-) create mode 100644 lib/livebook.ex create mode 100644 lib/livebook/helpers.ex create mode 100644 lib/livebook/module.ex delete mode 100644 lib/livebooks.ex diff --git a/README.md b/README.md index af23978..8eec58d 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ Welcome to the home for [my personal livebooks](https://github.com/christhekeele/livebooks/tree/latest/livebooks): Elixir experiments, guides, and accompanying source code. Their history can be accessed [on GitHub](https://github.com/christhekeele/livebooks) by the clicking the "view source" -`` icon next to the title of every page. Corrections, proposals, and discussions can be made in the GitHub [issues](https://github.com/christhekeele/livebooks/issues), [pull requests](https://github.com/christhekeele/livebooks/pulls), and [discussions](https://github.com/christhekeele/livebooks/discussions) pages respectively. +`` icon next to the title of every page. Corrections, proposals, and discussions can be made in the GitHub [pull requests](https://github.com/christhekeele/livebooks/pulls), [issues](https://github.com/christhekeele/livebooks/issues), and [discussions](https://github.com/christhekeele/livebooks/discussions) pages respectively. diff --git a/lib/livebook.ex b/lib/livebook.ex new file mode 100644 index 0000000..8ec7250 --- /dev/null +++ b/lib/livebook.ex @@ -0,0 +1,9 @@ +defmodule Livebook do + use Application + + @impl true + def start(_type, _args) do + [Livebook.Module] + |> Supervisor.start_link(strategy: :one_for_one, name: Livebook.Supervisor) + end +end diff --git a/lib/livebook/helpers.ex b/lib/livebook/helpers.ex new file mode 100644 index 0000000..1cdaca9 --- /dev/null +++ b/lib/livebook/helpers.ex @@ -0,0 +1,18 @@ +defmodule Livebook.Helpers do + @moduledoc """ + Common helpers for these livebooks. + + > #### `use Livebook.Helpers` {: .info} + > + > When you `use Livebook.Helpers`, the following will be + > imported into your livebook: + > + > - `Livebook.Module.defmodule/3`: an iterable version of `defmodule/2` + """ + + defmacro __using__(_opts \\ []) do + quote do + import Livebook.Module, only: [defmodule: 3] + end + end +end diff --git a/lib/livebook/module.ex b/lib/livebook/module.ex new file mode 100644 index 0000000..e42087b --- /dev/null +++ b/lib/livebook/module.ex @@ -0,0 +1,91 @@ +defmodule Livebook.Module do + require Matcha.Table.ETS + + @table_name __MODULE__ + + @doc false + def child_spec(options) do + %{ + id: __MODULE__, + start: {__MODULE__, :start_link, options} + } + end + + @doc false + def start_link(_options \\ []) do + if :ets.whereis(@table_name) == :undefined do + :ets.new(@table_name, [:ordered_set, :named_table, :public]) + # We don't actually need to start/supervise any process + :ignore + else + {:error, {:already_started, self()}} + end + end + + @doc """ + Defines a module that can be iterated upon in a livebook. + + When defining a module, you can specify a `v: integer` version for it. + You can then later redefine it in your livebook with an incremented version number + without error. + + Under the covers, the code provided to define each module is stored, + and retreived and prepended to the code in later versions. + This means that functions defined in previous versions can be patched without warning by + using `defoverridable/1` just before re-defining them, and invoked in the new definition + using `super/1`. Note that because `alias/2`, `require/2`, and `import/2` are lexically scoped, + they must be repeated every module definition. Module attributes (`@/1`), however, work as expected. + + Each version of the module is compiled and available as `"\#{module_name}.__v\#{version}"`, + then `alias/2`'d to `module_name` so later code works as expected. + + Inspired by [this macro](https://github.com/brettbeatty/experiments_elixir/blob/master/module_patching.livemd) + and [this forum post](https://elixirforum.com/t/how-do-i-redifine-modules-in-livebook/56442/2). + """ + defmacro defmodule( + _module_name = {:__aliases__, _, [alias | []]}, + _version_specifier = [v: version], + _code = [do: block] + ) + when version >= 0 do + module = Macro.expand({:__aliases__, [alias: false], [:"Elixir", alias]}, __CALLER__) + version = Macro.expand(version, __CALLER__) + version_namespace = "__v#{version}" |> String.to_atom() + module_namespace = module |> Module.concat(version_namespace) + add_version(module, version, block) + + code = + history(module, version) + |> Macro.prewalk(fn + aliases = {:__aliases__, meta, [^alias | rest]} -> + # IO.inspect(aliases, label: :FOUND) + {:__aliases__, meta, [:"Elixir", alias | rest]} + + code -> + code + end) + + :ok + + quote do + defmodule unquote(module_namespace), do: unquote(code) + alias unquote(module_namespace), as: unquote(module) + end + end + + @doc false + def add_version(module, version, block) do + :ets.insert(@table_name, {{module, version}, block}) + end + + @doc false + def history(module, since_version) do + Matcha.Table.ETS.select @table_name do + {{^module, version}, block} when version <= since_version -> block + end + |> Enum.flat_map(&unblock/1) + end + + defp unblock({:__block__, _meta, block}), do: block + defp unblock(ast), do: [ast] +end diff --git a/lib/livebooks.ex b/lib/livebooks.ex deleted file mode 100644 index dec0ef9..0000000 --- a/lib/livebooks.ex +++ /dev/null @@ -1,3 +0,0 @@ -defmodule Livebooks do - @moduledoc false -end diff --git a/livebooks/surreal.livemd b/livebooks/surreal.livemd index 7b0a8e6..a3c6dbd 100644 --- a/livebooks/surreal.livemd +++ b/livebooks/surreal.livemd @@ -1,11 +1,29 @@ # Surreal Numbers ```elixir -Mix.install([ - {:ets, "~> 0.9.0"}, - {:eternal, "~> 1.2"} -]) +Mix.install( + [ + {:ets, "~> 0.9.0"}, + {:eternal, "~> 1.2"}, + {:livebooks, github: "christhekeele/livebooks", branch: "latest"} + ], + force: true +) +``` + +## Video Format + +[Watch the ElixirConf 2022 lightning talk here!](https://www.youtube.com/watch?v=f1lNK5gDlwA&t=235s) + +## Helpers + +```elixir +use Livebook.Helpers +``` + +## Caching +```elixir require Logger defmodule Surreal.Guards do @@ -15,14 +33,14 @@ defmodule Surreal.Guards do end defmodule Surreal.Cache do - @name __MODULE__ + @table_name __MODULE__ def init() do - ETS.KeyValueSet.new!(name: @name, protection: :public) + ETS.KeyValueSet.new!(name: @table_name, protection: :public) end defp cache() do - ETS.KeyValueSet.wrap_existing!(@name) + ETS.KeyValueSet.wrap_existing!(@table_name) end def fetch(key) do @@ -50,10 +68,6 @@ end Surreal.Cache.init() ``` -## Video Format - -[Watch the ElixirConf 2022 lightning talk here!](https://www.youtube.com/watch?v=f1lNK5gDlwA&t=235s) - ## Axioms * Zero @@ -96,26 +110,23 @@ five_eighths = {[one_half], [three_quarters]} ## Module ```elixir -defmodule Surreal do - import Kernel, except: [-: 1, +: 2, -: 2, *: 2, /: 2, <>: 2] - import Surreal.Guards +defmodule Surreal, v: 1 do + @empty_set [] + @zero {@empty_set, @empty_set} + @one {[@zero], @empty_set} - @zero {[], []} - @one {[{[], []}], []} + IO.inspect(@zero) + IO.inspect(@one) end ``` ## Set Concatenation ```elixir -defmodule Surreal do - import Kernel, except: [-: 1, +: 2, -: 2, *: 2, /: 2, <>: 2] +defmodule Surreal, v: 2 do + import Kernel, except: [<>: 2] import Surreal.Guards - @empty_set [] - @zero {@empty_set, @empty_set} - @one {[@zero], @empty_set} - # Set concatenation def @empty_set <> surreals when is_set(surreals) do @@ -135,14 +146,10 @@ end ## Surreal Math ```elixir -defmodule Surreal do +defmodule Surreal, v: 3 do import Kernel, except: [-: 1, +: 2, -: 2, *: 2, /: 2, <>: 2] import Surreal.Guards - @empty_set [] - @zero {@empty_set, @empty_set} - @one {[@zero], @empty_set} - @doc """ Negates a surreal number. """ @@ -245,20 +252,6 @@ defmodule Surreal do def surreals1 * surreals2 when is_set(surreals1) and is_set(surreals2) do Enum.uniq(Enum.flat_map(surreals1, &__MODULE__.*(&1, surreals2))) end - - # Set concatenation - - def @empty_set <> surreals when is_set(surreals) do - surreals - end - - def surreals <> @empty_set when is_set(surreals) do - surreals - end - - def surreals1 <> surreals2 when is_set(surreals1) and is_set(surreals2) do - Enum.uniq(surreals1 ++ surreals2) - end end ``` @@ -290,13 +283,7 @@ neg_two |> IO.inspect() ```elixir require Logger -defmodule Surreal do - import Kernel, except: [-: 1, +: 2, -: 2, *: 2, /: 2, <>: 2] - import Surreal.Guards - - @zero {[], []} - @one {[{[], []}], []} - +defmodule Surreal, v: 4 do @doc """ Converts a number to a surreal number. @@ -374,151 +361,6 @@ defmodule Surreal do end) end end - - @doc """ - Concatenates two sets of surreals. - """ - - def [] <> surreals when is_set(surreals) do - surreals - end - - def surreals <> [] when is_set(surreals) do - surreals - end - - def surreals1 <> surreals2 when is_set(surreals1) and is_set(surreals2) do - Enum.uniq(surreals1 ++ surreals2) - end - - @doc """ - Negates a surreal number. - """ - - def -@zero do - @zero - end - - def -surreal when is_surreal(surreal) do - Logger.debug("negating: `#{inspect(surreal)}`") - - Surreal.Cache.try({:-, surreal}, fn -> - {left, right} = surreal - {-right, -left} - end) - end - - def -surreals when is_set(surreals) do - Enum.map(surreals, &-/1) - end - - @doc """ - Adds two surreal numbers. - """ - - def @zero + @zero do - @zero - end - - def surreal1 + surreal2 when is_surreal(surreal1) and is_surreal(surreal2) do - Logger.debug("adding: `#{inspect(surreal1)} + #{inspect(surreal2)}`") - - Surreal.Cache.try({:+, surreal1, surreal2}, fn -> - {left1, right1} = surreal1 - {left2, right2} = surreal2 - - left = (left1 + surreal2) <> (left2 + surreal1) - right = (right1 + surreal2) <> (right2 + surreal1) - {left, right} - end) - end - - def surreals + surreal when is_set(surreals) and is_surreal(surreal) do - Enum.uniq(Enum.map(surreals, &__MODULE__.+(&1, surreal))) - end - - def surreal + surreals when is_set(surreals) and is_surreal(surreal) do - Enum.uniq(Enum.map(surreals, &__MODULE__.+(surreal, &1))) - end - - def surreals1 + surreals2 when is_set(surreals1) and is_set(surreals2) do - Enum.uniq(Enum.flat_map(surreals1, &__MODULE__.+(&1, surreals2))) - end - - @doc """ - Subtracts two surreal numbers. - """ - - def surreal1 - surreal2 when is_surreal(surreal1) and is_surreal(surreal2) do - Logger.debug("subtracting: `#{inspect(surreal1)} - #{inspect(surreal2)}`") - - Surreal.Cache.try({:-, surreal1, surreal2}, fn -> - surreal1 + -surreal2 - end) - end - - def surreals - surreal when is_set(surreals) and is_surreal(surreal) do - Enum.uniq(Enum.map(surreals, &__MODULE__.-(&1, surreal))) - end - - def surreal - surreals when is_set(surreals) and is_surreal(surreal) do - Enum.uniq(Enum.map(surreals, &__MODULE__.-(surreal, &1))) - end - - def surreals1 - surreals2 when is_set(surreals1) and is_set(surreals2) do - Enum.uniq(Enum.flat_map(surreals1, &__MODULE__.-(&1, surreals2))) - end - - @doc """ - Multiplies two surreal numbers. - """ - - def @zero * surreal when is_surreal(surreal) do - @zero - end - - def surreal * @zero when is_surreal(surreal) do - @zero - end - - def @one * surreal when is_surreal(surreal) do - surreal - end - - def surreal * @one when is_surreal(surreal) do - surreal - end - - def surreal1 * surreal2 when is_surreal(surreal1) and is_surreal(surreal2) do - Logger.debug("multiplying: `#{inspect(surreal1)} * #{inspect(surreal2)}`") - - Surreal.Cache.try({:*, surreal1, surreal2}, fn -> - {left1, right1} = surreal1 - {left2, right2} = surreal2 - - left = - (left1 * surreal2 + surreal1 * left2 - left1 * left2) <> - (right1 * surreal2 + surreal1 * right2 - right1 * right2) - - right = - (left1 * surreal2 + surreal1 * right2 - left1 * right2) <> - (surreal1 * left2 + right1 * surreal2 - right1 * left2) - - {left, right} - end) - end - - def surreals * surreal when is_set(surreals) and is_surreal(surreal) do - Enum.uniq(Enum.map(surreals, &__MODULE__.*(&1, surreal))) - end - - def surreal * surreals when is_set(surreals) and is_surreal(surreal) do - Enum.uniq(Enum.map(surreals, &__MODULE__.*(surreal, &1))) - end - - def surreals1 * surreals2 when is_set(surreals1) and is_set(surreals2) do - Enum.uniq(Enum.flat_map(surreals1, &__MODULE__.*(&1, surreals2))) - end end ``` diff --git a/mix.exs b/mix.exs index 52d31d7..44ec3c6 100644 --- a/mix.exs +++ b/mix.exs @@ -61,24 +61,41 @@ defmodule Livebooks.MixProject do ] def cli do - test_by_default = aliases() |> Keyword.keys() |> Map.new(&{&1, :test}) - doc_overrides = [:build, :docs, :static] |> Map.new(&{&1, :docs}) + test_tasks = + [ + :check, + :lint, + :"lint.compile", + :"lint.deps", + :"lint.format", + :"lint.style", + :typecheck, + :"typecheck.build-cache", + :"typecheck.clean", + :"typecheck.explain", + :"typecheck.run", + :"test.coverage", + :"test.coverage.report" + ] + |> Map.new(&{&1, :test}) + + doc_tasks = [:build, :docs, :static, :"hex.publish"] |> Map.new(&{&1, :docs}) preferred_envs = - test_by_default - |> Map.merge(doc_overrides) + %{} + |> Map.merge(test_tasks) + |> Map.merge(doc_tasks) |> Map.to_list() [ - default_task: "docs", + default_env: :dev, preferred_envs: preferred_envs ] end - def application(), - do: [ - extra_application: [:logger] - ] + def application() do + [extra_application: [:logger], mod: {Livebook, []}] + end defp aliases, do: [ @@ -151,6 +168,8 @@ defmodule Livebooks.MixProject do defp deps(), do: [ + {:matcha, "~> 0.1", github: "christhekeele/matcha", branch: "latest"}, + # Site generation {:ex_doc, "~> 0.32", only: @doc_envs, runtime: false}, # Static analysis {:credo, "~> 1.7", only: @dev_envs, runtime: false}, diff --git a/mix.lock b/mix.lock index 30ef2e6..1a6645e 100644 --- a/mix.lock +++ b/mix.lock @@ -11,5 +11,7 @@ "makeup": {:hex, :makeup, "1.1.1", "fa0bc768698053b2b3869fa8a62616501ff9d11a562f3ce39580d60860c3a55e", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "5dc62fbdd0de44de194898b6710692490be74baa02d9d108bc29f007783b0b48"}, "makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"}, "makeup_erlang": {:hex, :makeup_erlang, "0.1.5", "e0ff5a7c708dda34311f7522a8758e23bfcd7d8d8068dc312b5eb41c6fd76eba", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "94d2e986428585a21516d7d7149781480013c56e30c6a233534bedf38867a59a"}, + "matcha": {:git, "https://github.com/christhekeele/matcha.git", "965b292ca2cf070fd52dddb94f8eb227fe52f29b", [branch: "latest"]}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, + "recon": {:hex, :recon, "2.5.5", "c108a4c406fa301a529151a3bb53158cadc4064ec0c5f99b03ddb8c0e4281bdf", [:mix, :rebar3], [], "hexpm", "632a6f447df7ccc1a4a10bdcfce71514412b16660fe59deca0fcf0aa3c054404"}, }