Skip to content

Commit

Permalink
Try module patching helpers
Browse files Browse the repository at this point in the history
  • Loading branch information
christhekeele committed Apr 22, 2024
1 parent a03e72a commit ba588e5
Show file tree
Hide file tree
Showing 8 changed files with 220 additions and 225 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -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.
9 changes: 9 additions & 0 deletions lib/livebook.ex
Original file line number Diff line number Diff line change
@@ -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
18 changes: 18 additions & 0 deletions lib/livebook/helpers.ex
Original file line number Diff line number Diff line change
@@ -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
96 changes: 96 additions & 0 deletions lib/livebook/module.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
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_alias = Macro.expand({:__aliases__, [alias: false], [:"Elixir", alias]}, __CALLER__)
version = Macro.expand(version, __CALLER__)
version_namespace = "__v#{version}" |> String.to_atom()
module = module_alias |> Module.concat(version_namespace)
add_version(module_alias, version, block)

code =
history(module_alias, version)
|> Macro.prewalk(fn
aliases = {:__aliases__, meta, [^alias | rest]} ->
# IO.inspect(aliases, label: :FOUND)
{:__aliases__, meta, [:"Elixir", alias | rest]}

code ->
code
end)

quote do
# :code.purge(unquote(module))
# :code.delete(unquote(module))
defmodule unquote(module), do: unquote(code)
alias unquote(module), as: unquote(module_alias)
end
|> tap(
&(&1
|> Macro.to_string()
|> IO.puts())
)
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
3 changes: 0 additions & 3 deletions lib/livebooks.ex

This file was deleted.

Loading

0 comments on commit ba588e5

Please sign in to comment.