diff --git a/.watermark.json b/.watermark.json deleted file mode 100644 index d1d10c1..0000000 --- a/.watermark.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "source_repo": "elixir-lang/elixir", - "last_digest_sha": "55a3899e75efd579723da1927500b82f206329e4", - "last_digest_at": "2026-04-30T14:01:00Z", - "last_refresh_sha": null, - "last_refresh_at": null -} diff --git a/changelog/.gitkeep b/changelog/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/changelog/2026-04-30.md b/changelog/2026-04-30.md deleted file mode 100644 index 8970924..0000000 --- a/changelog/2026-04-30.md +++ /dev/null @@ -1,44 +0,0 @@ -# Elixir Digest — 2026-04-30 - -## Type System / Gradual Typing - -### #15324 — Fix invalid type inference on size compare in guards -- **Author:** lukaszsamson -- **Merged:** 2026-04-29 -- Mirror expressions like `tuple_size(x) >= 2` and `2 <= tuple_size(x)` were producing different type inferences. -- Fix introduces `mirror_order/1` helper that correctly flips comparison operators when operands are swapped. -- Impact: Guards with size on the right now correctly infer the same type as left-sided equivalents. - -### #15322 — Fix map union optimization for open maps -- **Author:** gldubc (Guillaume Duboc) -- **Merged:** 2026-04-29 -- Optimizer subtype shortcut incorrectly concluded open map contained by closed map when field types matched. -- Open maps can have extra keys — fix rejects shortcut on tag (open/closed) mismatch. -- Impact: Fixes false type narrowing with unions of open and closed map types. - -### #15319 — Fix map difference union optimization -- **Author:** gldubc (Guillaume Duboc) -- **Merged:** 2026-04-28 -- Disabled `:union` case of single-key open-map difference optimization. -- Could build invalid union literals causing double negation to leave phantom empty maps. - -## Core / Runtime - -### #15306 — Fix reentrancy of Code.eval_* -- **Author:** lukaszsamson -- **Merged:** 2026-04-28 -- Nested `Code.eval_string` clobbered outer eval's `dbg_callback` and `?elixir_eval_env` in process dict. -- Fix: save/restore pattern (save before, restore in `after` block). -- Discussion: Jonatan Kłosko caught test was not actually testing nested eval; José simplified implementation. -- Lesson: Process dictionary as implicit state = reentrancy bugs. Deliberate trade-off for version decoupling. - -### #15316 — Consistently return path as binary in relative_to_cwd -- **Author:** lukaszsamson -- **Merged:** 2026-04-28 -- `Path.relative_to_cwd/1` could return chardata on `:file.get_cwd` failure. -- All code paths now normalize to binary. - -## Patterns to Extract - -- **Process dict save/restore for reentrancy** (#15306): When using process dict as implicit state, always save/restore in try/after to handle reentrancy. The Elixir team chose this over closures to avoid coupling eval to specific Elixir versions. -- **Set-theoretic type system edge cases** (#15322, #15319): Open vs closed map distinction is subtle in BDD-based type representations. Subtype checks must respect structural tags, not just field types. diff --git a/enhance_macros3.py b/enhance_macros3.py deleted file mode 100644 index e5bcab6..0000000 --- a/enhance_macros3.py +++ /dev/null @@ -1,749 +0,0 @@ -#!/usr/bin/env python3 -"""Add When to Use / When NOT to Use sections to macros.md -Parts: 0=preamble, 1=section1, 2=section2, ..., 12=section12""" - -with open("patterns/macros.md", "r") as f: - content = f.read() - -separator = "\n\n---\n\n" -parts = content.split(separator) -assert len(parts) == 13, f"Expected 13 parts, got {len(parts)}" - -# Index 1-12 maps to sections 1-12 -when_sections = { - 1: ''' -### When to Use - -**Triggers:** -- A macro must behave differently in guards vs normal code -- You're writing an operator-like macro that users will put in guard clauses -- The same syntax needs different compilation strategies per context - -**Example — before:** -```elixir -# Macro that only works in normal code, crashes in guards -defmacro my_or(left, right) do - quote do - case unquote(left) do - x when x in [false, nil] -> unquote(right) - x -> x - end - end -end -``` - -**Example — after:** -```elixir -defmacro my_or(left, right) do - case __CALLER__.context do - nil -> quote do - case unquote(left) do - x when x in [false, nil] -> unquote(right) - x -> x - end - end - :guard -> quote(do: :erlang.orelse(unquote(left), unquote(right))) - :match -> raise ArgumentError, "cannot use or/2 in match" - end -end -``` - -### When NOT to Use - -**Don't use this when:** -- Your macro will never be used in guards (most macros) -- A simple `defguard` would suffice for guard-only usage - -**Over-application example:** -```elixir -# Checking __CALLER__.context in a macro that just generates module-level code -defmacro define_schema(fields) do - case __CALLER__.context do - nil -> generate_schema(fields) - :guard -> raise "can't use in guard" # Unnecessary - :match -> raise "can't use in match" # Unnecessary - end -end -``` - -**Better alternative:** -```elixir -defmacro define_schema(fields) do - generate_schema(fields) -end -``` - -**Why:** Context-checking adds complexity. Only use it when there's a legitimate code path for guards or matches. Module-level macros like schema definitions will never appear in guard context.''', - - 2: ''' -### When to Use - -**Triggers:** -- You need a guard-safe check that combines multiple guard expressions -- Multiple modules share the same guard condition -- The guard logic is complex enough to warrant a name - -**Example — before:** -```elixir -# Repeating guard logic everywhere -def process(n) when is_integer(n) and rem(n, 2) == 0 do - # handle even... -end - -def validate(n) when is_integer(n) and rem(n, 2) == 0 do - # validate even... -end -``` - -**Example — after:** -```elixir -defmodule Guards do - defguard is_even(n) when is_integer(n) and rem(n, 2) == 0 -end - -import Guards - -def process(n) when is_even(n), do: # ... -def validate(n) when is_even(n), do: # ... -``` - -### When NOT to Use - -**Don't use this when:** -- The check isn't guard-safe (calls functions, uses try/rescue, etc.) -- The guard is used in one place and is already readable -- You need runtime logic like database lookups in the check - -**Over-application example:** -```elixir -# Trying to use defguard for something that needs runtime state -defguard is_admin(user_id) when user_id in @admin_ids -# @admin_ids is a module attribute — frozen at compile time! -``` - -**Better alternative:** -```elixir -def admin?(user_id), do: user_id in load_admin_ids() - -def process(request) do - if admin?(request.user_id), do: # ... -end -``` - -**Why:** `defguard` expressions are evaluated at compile time with only BIF access. If your "guard" needs runtime data, config, or database state, it's not a guard — it's a function.''', - - 3: ''' -### When to Use - -**Triggers:** -- A macro receives arguments that should be evaluated once in the expansion -- You're generating code that interpolates compile-time values into runtime code -- The macro argument has side effects or is expensive to compute - -**Example — before:** -```elixir -# unquote(fields) evaluated twice -defmacro setup(fields) do - quote do - validated = validate(unquote(fields)) - @fields unquote(fields) # fields expression runs again! - end -end -``` - -**Example — after:** -```elixir -defmacro setup(fields) do - quote bind_quoted: [fields: fields] do - validated = validate(fields) - @fields fields # Same bound value, no double-evaluation - end -end -``` - -### When NOT to Use - -**Don't use this when:** -- You need to unquote into a pattern match position (bind_quoted can't do this) -- The expression is a simple literal or variable with no side effects -- You're building complex AST where you need fine-grained unquote placement - -**Over-application example:** -```elixir -# bind_quoted fails in pattern positions -defmacro match_field(field, value) do - quote bind_quoted: [field: field, value: value] do - def handle(%{field => result}) when result == value do - result - end - end -end -``` - -**Better alternative:** -```elixir -defmacro match_field(field, value) do - quote do - def handle(%{unquote(field) => result}) when result == unquote(value) do - result - end - end -end -``` - -**Why:** `bind_quoted` wraps values in regular variable bindings, which can't appear in pattern match positions. When you need to inject into patterns or guards, use direct `unquote`.''', - - 4: ''' -### When to Use - -**Triggers:** -- A macro must inject code that references a variable from the caller's scope -- Building test/assertion macros where you need access to the test binding -- Module-level macros that accumulate into a module attribute the caller defines - -**Example — before:** -```elixir -# Hygienic variable — creates a NEW binding, doesn't access caller's -defmacro assert_stored(key) do - quote do - assert store[unquote(key)] # 'store' is hygienic — undefined in caller! - end -end -``` - -**Example — after:** -```elixir -defmacro assert_stored(key) do - quote do - assert var!(store)[unquote(key)] # Accesses caller's 'store' variable - end -end -``` - -### When NOT to Use - -**Don't use this when:** -- You can pass the value as a macro argument instead -- The macro doesn't need to reference caller-scope variables -- You're creating new bindings (just let hygiene work naturally) - -**Over-application example:** -```elixir -# Using var! when the value could just be passed as an argument -defmacro double_it do - quote do - var!(x) * 2 # Assumes caller has 'x' — fragile! - end -end - -# Caller must have 'x' defined -x = 5 -double_it() # => 10 -``` - -**Better alternative:** -```elixir -defmacro double_it(value) do - quote do - unquote(value) * 2 - end -end - -double_it(5) # => 10, explicit, no hidden dependencies -``` - -**Why:** Every `var!` creates an invisible contract between macro and caller. Prefer explicit arguments. Reserve `var!` for cases where the contract is part of the documented API (like ExUnit's test context).''', - - 5: ''' -### When to Use - -**Triggers:** -- Your macro receives user input that could be an alias, module attribute, or compile-time expression -- You need to make compile-time decisions based on what type of thing was passed -- Distinguishing between module names and runtime expressions in macro arguments - -**Example — before:** -```elixir -# Trying to pattern-match on the raw AST — breaks with aliases -defmacro wrap_error(module) when is_atom(module) do - # Never matches! Aliases are {:__aliases__, _, [...]} in AST - quote do: %{__exception__: true, __struct__: unquote(module)} -end -``` - -**Example — after:** -```elixir -defmacro wrap_error(module_or_msg) do - case Macro.expand(module_or_msg, __CALLER__) do - atom when is_atom(atom) -> - quote do: unquote(atom).exception([]) - _ -> - quote do: RuntimeError.exception(unquote(module_or_msg)) - end -end -``` - -### When NOT to Use - -**Don't use this when:** -- The macro argument is always a literal (string, number) -- You don't need compile-time branching on the argument's type -- The argument should remain unevaluated (lazy evaluation semantics) - -**Over-application example:** -```elixir -# Expanding an argument that's meant to be a runtime expression -defmacro log(message) do - expanded = Macro.expand(message, __CALLER__) - quote do - Logger.info(unquote(expanded)) - end -end -``` - -**Better alternative:** -```elixir -defmacro log(message) do - quote do - Logger.info(unquote(message)) # Let it evaluate at runtime - end -end -``` - -**Why:** `Macro.expand` resolves compile-time constructs (aliases, module attributes). Expanding function calls or complex expressions can produce confusing results. Only expand when you need to determine the *type* of the argument at compile time.''', - - 6: ''' -### When to Use - -**Triggers:** -- Your macro defines module-level constructs (functions, types, modules) -- The macro makes no sense inside a guard or pattern match -- Early failure with a clear message prevents confusing downstream errors - -**Example — before:** -```elixir -# No context validation — weird error deep in expansion -defmacro defroute(path, handler) do - quote do - @routes [{unquote(path), unquote(handler)} | @routes] - end -end - -# User accidentally writes: -def check(x) when defroute("/foo", Foo), do: x # Cryptic error -``` - -**Example — after:** -```elixir -defmacro defroute(path, handler) do - assert_no_match_or_guard_scope(__CALLER__.context, "defroute/2") - quote do - @routes [{unquote(path), unquote(handler)} | @routes] - end -end - -# Now gives: "cannot invoke defroute/2 inside a guard" -``` - -### When NOT to Use - -**Don't use this when:** -- Your macro IS designed to work in guards (use `defguard` or handle context) -- Your macro is designed to work in match context (like custom patterns) -- The macro is simple enough that misuse produces a clear error naturally - -**Over-application example:** -```elixir -# Adding guard assertion to a macro that could legitimately work anywhere -defmacro debug(expr) do - assert_no_match_or_guard_scope(__CALLER__.context, "debug/1") - quote do - IO.inspect(unquote(expr), label: unquote(Macro.to_string(expr))) - end -end -``` - -**Better alternative:** -```elixir -# Let it work anywhere it naturally can -defmacro debug(expr) do - quote do - IO.inspect(unquote(expr), label: unquote(Macro.to_string(expr))) - end -end -``` - -**Why:** Only add context assertions when misuse would produce confusing errors. If your macro naturally fails with a clear error in the wrong context, the assertion adds noise without value.''', - - 7: ''' -### When to Use - -**Triggers:** -- You're building a type system or dispatch mechanism that operates on open-ended types -- Users need to add behavior for their own types without modifying your library -- You need dynamic dispatch based on the data type with compile-time consolidation - -**Example — before:** -```elixir -# Manual dispatch via case — closed, must modify to extend -def serialize(data) do - case data do - %User{} -> serialize_user(data) - %Post{} -> serialize_post(data) - _ -> raise "don't know how to serialize" - end -end -``` - -**Example — after:** -```elixir -defprotocol Serializable do - @doc "Converts a data structure to a wire format" - def serialize(data) -end - -defimpl Serializable, for: User do - def serialize(%User{name: name, email: email}), do: %{name: name, email: email} -end - -# Anyone can add implementations for their own types -``` - -### When NOT to Use - -**Don't use this when:** -- You have a closed set of types that won't be extended by users -- Simple pattern matching or behaviours solve the problem -- The dispatch doesn't depend on the first argument's type - -**Over-application example:** -```elixir -# Protocol for internal types that will never be extended -defprotocol InternalFormat do - def format(thing) -end - -defimpl InternalFormat, for: [Map, List, BitString] do - # Only ever these three types, controlled by us -end -``` - -**Better alternative:** -```elixir -# Simple function with pattern matching — less machinery -def format(%{} = map), do: # ... -def format(list) when is_list(list), do: # ... -def format(binary) when is_binary(binary), do: # ... -``` - -**Why:** Protocols add indirection and compilation complexity (consolidation). If the type set is closed and you control all implementations, pattern matching is simpler, faster, and easier to understand.''', - - 8: ''' -### When to Use - -**Triggers:** -- A protocol should handle ANY value rather than crashing on unknown types -- The generic behavior is genuinely useful (inspect, encode, display) -- You want a safe default that users can override for specific types - -**Example — before:** -```elixir -defprotocol Displayable do - def display(term) -end - -# Every new struct crashes until someone adds an implementation: -# ** (Protocol.UndefinedError) protocol Displayable not implemented for %MyStruct{} -``` - -**Example — after:** -```elixir -defprotocol Displayable do - @fallback_to_any true - def display(term) -end - -defimpl Displayable, for: Any do - def display(term), do: inspect(term) # Reasonable fallback -end -``` - -### When NOT to Use - -**Don't use this when:** -- The protocol operation doesn't make sense for arbitrary types -- Silently returning a default would hide bugs -- You want to force implementors to think about their implementation - -**Over-application example:** -```elixir -defprotocol Saveable do - @fallback_to_any true - def save(data, repo) -end - -defimpl Saveable, for: Any do - def save(_data, _repo), do: :ok # Silently does nothing! -end - -# Dangerous: %UnknownStruct{} |> Saveable.save(repo) succeeds but saves nothing -``` - -**Better alternative:** -```elixir -defprotocol Saveable do - # No fallback — forces explicit implementation - def save(data, repo) -end - -# Clear error on missing implementation: -# ** (Protocol.UndefinedError) protocol Saveable not implemented for %Foo{} -``` - -**Why:** Fallbacks that silently succeed are a bug factory. Use `@fallback_to_any` only when the default behavior is *genuinely useful* (like `Inspect`), not when "do nothing" masks errors.''', - - 9: ''' -### When to Use - -**Triggers:** -- Your module needs to inject behaviours, default function implementations, or compile hooks -- Users need a one-line "opt in" that sets up complex module infrastructure -- The setup requires `@behaviour`, `@before_compile`, `defoverridable`, or module attributes - -**Example — before:** -```elixir -# User must remember all the boilerplate -defmodule MyWorker do - @behaviour GenServer - - def child_spec(init_arg) do - %{id: __MODULE__, start: {__MODULE__, :start_link, [init_arg]}} - end - - def init(state), do: {:ok, state} - # ... more defaults ... -end -``` - -**Example — after:** -```elixir -defmodule MyWorker do - use GenServer - - # All boilerplate injected, just implement what you need - def init(state), do: {:ok, state} -end -``` - -### When NOT to Use - -**Don't use this when:** -- `import` or `alias` is all you need (no module attributes, no callbacks) -- The module doesn't need to inject code — just provides functions -- You're using `use` to inject large amounts of invisible code that surprises users - -**Over-application example:** -```elixir -# use for something that should just be import -defmodule MyHelpers do - defmacro __using__(_opts) do - quote do - import MyHelpers # That's literally all it does - end - end -end - -# User writes: use MyHelpers -# When they could just write: import MyHelpers -``` - -**Better alternative:** -```elixir -# Just tell users to import directly -defmodule MyHelpers do - def format_date(date), do: # ... - def format_money(amount), do: # ... -end - -# In user's module: -import MyHelpers -``` - -**Why:** `use` implies "this module needs setup that goes beyond importing functions." If all you're doing is importing, `use` adds a layer of indirection that obscures what's happening. Reserve `use` for genuine module setup.''', - - 10: ''' -### When to Use - -**Triggers:** -- You have compile-time-known literal values that benefit from validation at compile time -- A domain has a specific syntax for literals (dates, regex, URIs, colors) -- You want zero runtime parsing cost for constant values - -**Example — before:** -```elixir -# Runtime parsing — fails at runtime, no compile-time validation -def deadline do - Date.from_iso8601!("2024-13-45") # Explodes at runtime -end -``` - -**Example — after:** -```elixir -def deadline do - ~D[2024-13-45] # Compile error! Invalid date caught immediately -end -``` - -### When NOT to Use - -**Don't use this when:** -- Values come from runtime input (user data, config files, databases) -- The syntax doesn't provide meaningful compile-time validation -- A regular function or struct literal is equally clear - -**Over-application example:** -```elixir -# Sigil for something with no compile-time validation benefit -defmacro sigil_u({:<<>>, _, [string]}, []) do - quote do: String.upcase(unquote(string)) -end - -name = ~u"hello" # Just uppercases a string... why not String.upcase("hello")? -``` - -**Better alternative:** -```elixir -name = String.upcase("hello") -``` - -**Why:** Sigils shine when they validate or transform at compile time in ways that prevent runtime errors. A sigil that just wraps a function call without validation adds syntax without value.''', - - 11: ''' -### When to Use - -**Triggers:** -- You want a zero-cost syntactic transformation (no runtime dispatch) -- The transformation is purely structural (rewriting argument positions) -- An operator or DSL benefits from left-to-right readability - -**Example — before:** -```elixir -# Deeply nested function calls — read inside-out -String.trim(String.downcase(String.replace(input, "_", " "))) -``` - -**Example — after:** -```elixir -input -|> String.replace("_", " ") -|> String.downcase() -|> String.trim() -``` - -### When NOT to Use - -**Don't use this when:** -- The pipe has only one step (just call the function directly) -- The piped value isn't the first argument (requires anonymous function wrappers) -- You're piping into a macro that needs special AST handling - -**Over-application example:** -```elixir -# Single-step pipe — adds noise -user -|> Map.get(:name) - -# Piping where the value isn't first argument -data -|> Jason.encode!() -|> send_to(socket) # Is this send_to(encoded, socket)? Unclear. -``` - -**Better alternative:** -```elixir -# Single step — just call it -name = Map.get(user, :name) - -# When argument position is unclear, break the pipe -encoded = data |> build_map() |> Jason.encode!() -send_to(socket, encoded) # Clear which arg is which -``` - -**Why:** Pipes optimize for readability of *sequential transformations*. When the data doesn't flow naturally as the first argument, or there's only one step, the pipe adds syntactic overhead without improving clarity.''', - - 12: ''' -### When to Use - -**Triggers:** -- Your macro generates variable bindings in quoted code -- You need `n` variables for a generated function clause or pattern -- The macro expands in user code where variable names could clash - -**Example — before:** -```elixir -# Hardcoded variable names — can clash with caller's variables -defmacro curry(fun, arity) do - args = Enum.map(1..arity, fn i -> Macro.var(:"arg\#{i}", __MODULE__) end) - quote do - fn unquote_splicing(args) -> unquote(fun).(unquote_splicing(args)) end - end -end -``` - -**Example — after:** -```elixir -defmacro curry(fun, arity) do - args = Macro.generate_unique_arguments(arity, __CALLER__.module) - quote do - fn unquote_splicing(args) -> unquote(fun).(unquote_splicing(args)) end - end -end -``` - -### When NOT to Use - -**Don't use this when:** -- You're using `bind_quoted` (it handles hygiene for you) -- The variable is accessed via `var!` (intentionally unhygienic) -- You only need one variable (a simple `quote do: var = ... end` is hygienic by default) - -**Over-application example:** -```elixir -# Using generate_unique_arguments for a single binding -defmacro time_it(expr) do - [start] = Macro.generate_unique_arguments(1, __CALLER__.module) - quote do - unquote(start) = System.monotonic_time() - result = unquote(expr) - IO.puts("Took \#{System.monotonic_time() - unquote(start)}") - result - end -end -``` - -**Better alternative:** -```elixir -# Regular quote hygiene handles single variables fine -defmacro time_it(expr) do - quote do - start = System.monotonic_time() - result = unquote(expr) - IO.puts("Took \#{System.monotonic_time() - start}") - result - end -end -``` - -**Why:** Variables created in `quote` are already hygienic by default — they can't clash with caller variables. `generate_unique_arguments` is needed when you're generating *multiple* variables dynamically (e.g., function parameters for a generated clause) where you need distinct names that also interoperate correctly.''', -} - -for i in range(len(parts)): - if i in when_sections: - parts[i] = parts[i].rstrip() + "\n\n" + when_sections[i].strip() - -output = separator.join(parts) + "\n" - -with open("patterns/macros.md", "w") as f: - f.write(output) - -print(f"Done! {len(output)} chars") diff --git a/enhance_modules.py b/enhance_modules.py deleted file mode 100644 index 3e9f0c3..0000000 --- a/enhance_modules.py +++ /dev/null @@ -1,425 +0,0 @@ -#!/usr/bin/env python3 -"""Add When to Use / When NOT to Use sections to modules.md""" - -with open("patterns/modules.md", "r") as f: - content = f.read() - -separator = "\n\n---\n\n" -parts = content.split(separator) -print(f"Found {len(parts)} parts") - -when_sections = { - 1: ''' -### When to Use - -**Triggers:** -- Your module has grown beyond ~300 lines with distinct sub-responsibilities -- External code only needs the parent module but implementation is complex -- You find yourself prefixing private functions with a concept name (e.g., `scope_push`, `scope_pop`) - -**Example — before:** -```elixir -# Everything crammed into one flat module -defmodule MyApp.Router do - # 800 lines mixing route compilation, scope tracking, and helper generation - def compile_route(...), do: # ... - def push_scope(...), do: # ... - def pop_scope(...), do: # ... - def generate_helper(...), do: # ... -end -``` - -**Example — after:** -```elixir -# Parent module is the public API -defmodule MyApp.Router do - # Public API delegates to focused submodules - def compile(routes), do: MyApp.Router.Compiler.compile(routes) -end - -# Submodules handle implementation -defmodule MyApp.Router.Compiler do - @moduledoc false - # ... -end - -defmodule MyApp.Router.Scope do - @moduledoc false - # ... -end -``` - -### When NOT to Use - -**Don't use this when:** -- The module is small and cohesive (< 200 lines) -- Nesting would exceed 3 levels (`A.B.C.D` is usually too deep) -- The "submodule" has its own independent public API (make it a sibling instead) - -**Over-application example:** -```elixir -# Over-nesting a simple utility -defmodule MyApp.Utils.String.Formatting.Case do - def upcase(s), do: String.upcase(s) -end -``` - -**Better alternative:** -```elixir -defmodule MyApp.StringUtils do - def upcase(s), do: String.upcase(s) -end -``` - -**Why:** Nesting should reflect genuine conceptual hierarchy. If you're creating submodules for 2-3 functions that don't have independent complexity, you're adding navigational overhead without architectural benefit.''', - - 2: ''' -### When to Use - -**Triggers:** -- You're writing a new module and need to decide function ordering -- A module has grown organically and functions are scattered randomly -- You're reviewing code and finding it hard to locate the public API - -**Example — before:** -```elixir -defmodule UserService do - defp hash_password(pw), do: # ... - - def create(attrs) do - # uses hash_password - end - - def start_link(opts), do: GenServer.start_link(__MODULE__, opts) - - defp validate(attrs), do: # ... - - def get(id), do: # ... -end -``` - -**Example — after:** -```elixir -defmodule UserService do - # Lifecycle - def start_link(opts), do: GenServer.start_link(__MODULE__, opts) - - # Public API - def create(attrs), do: # ... - def get(id), do: # ... - - # Private helpers - defp validate(attrs), do: # ... - defp hash_password(pw), do: # ... -end -``` - -### When NOT to Use - -**Don't use this when:** -- You have a tiny module (< 5 functions) where ordering doesn't matter much -- The module is a pure data module (just a struct + typespec) -- "Logical grouping" puts closely related public+private pairs together for readability - -**Over-application example:** -```elixir -# Forcing start_link to the top in a module that isn't an OTP process -defmodule MyApp.Parser do - # This module has no lifecycle — don't force OTP ordering - def start_link(_), do: raise "not a process" # Just to match the pattern? - def parse(input), do: # ... -end -``` - -**Better alternative:** -```elixir -defmodule MyApp.Parser do - @moduledoc "Parses input format X into structs" - - def parse(input), do: # ... - def parse!(input), do: # ... - - defp tokenize(input), do: # ... -end -``` - -**Why:** The ordering convention exists to make OTP-aware modules predictable. For non-OTP modules, lead with the primary public function (the one callers reach for first) and let the rest follow logically.''', - - 3: ''' -### When to Use - -**Triggers:** -- A module exists purely for internal code organization -- Users of your library should never call this module directly -- The module is a helper that could change or disappear between versions - -**Example — before:** -```elixir -defmodule MyApp.Repo.QueryBuilder do - @moduledoc """ - Builds Ecto queries for the Repo module. - """ - # Now appears in docs, users try to call it directly -end -``` - -**Example — after:** -```elixir -defmodule MyApp.Repo.QueryBuilder do - @moduledoc false - # Hidden from docs, clearly internal -end -``` - -### When NOT to Use - -**Don't use this when:** -- The module is part of your public API (even if rarely used) -- Users need to implement callbacks or extend the module -- The module defines a behaviour or protocol that others implement - -**Over-application example:** -```elixir -# Hiding a module that users actually need -defmodule MyApp.Errors do - @moduledoc false # But users need to pattern-match on these! - - defmodule NotFound do - defexception [:message] - end -end -``` - -**Better alternative:** -```elixir -defmodule MyApp.Errors do - @moduledoc "Error types raised by MyApp operations." - - defmodule NotFound do - @moduledoc "Raised when a resource cannot be found." - defexception [:message] - end -end -``` - -**Why:** `@moduledoc false` means "this is not for you." If users catch your exceptions or match on your structs, they need documentation. Hide implementation details, not public contracts.''', - - 4: ''' -### When to Use - -**Triggers:** -- Your struct has fields that make no sense as `nil` (creating one without them is a bug) -- You're modeling a value object where all fields define its identity -- Incomplete structs would cause confusing runtime errors later - -**Example — before:** -```elixir -defmodule Order do - defstruct [:id, :customer_id, :items, :total] - # Can create %Order{} with everything nil — meaningless -end -``` - -**Example — after:** -```elixir -defmodule Order do - @enforce_keys [:customer_id, :items, :total] - defstruct [:id | @enforce_keys] - # %Order{} without required fields → immediate compile/runtime error -end -``` - -### When NOT to Use - -**Don't use this when:** -- The struct is built incrementally (e.g., a changeset or builder pattern) -- Most fields have sensible defaults -- The struct represents configuration where partial specs are valid - -**Over-application example:** -```elixir -# Enforcing keys on a struct that's built in stages -defmodule FormState do - @enforce_keys [:step, :name, :email, :address, :payment] - defstruct @enforce_keys - # Can't create a partial form state for step 1! -end -``` - -**Better alternative:** -```elixir -defmodule FormState do - defstruct step: 1, name: nil, email: nil, address: nil, payment: nil - # Built incrementally as user progresses through steps -end -``` - -**Why:** `@enforce_keys` is for structs that represent *complete* values. If your struct represents an evolving state or has legitimate intermediate forms, enforcing all keys makes construction impossible at early stages.''', - - 5: ''' -### When to Use - -**Triggers:** -- Your `use` macro needs to give the caller access to specific functions -- You want to control exactly which functions enter the caller's namespace -- The imported functions are central to the DSL or workflow the module enables - -**Example — before:** -```elixir -defmacro __using__(_opts) do - quote do - # Imports EVERYTHING from three modules — namespace soup - import MyApp.Router.Helpers - import MyApp.Router.Scoping - import MyApp.Router.Compilation - end -end -``` - -**Example — after:** -```elixir -defmacro __using__(_opts) do - quote do - import MyApp.Router, only: [get: 2, post: 2, resources: 2, scope: 2] - import MyApp.Conn, only: [assign: 3, put_status: 2] - end -end -``` - -### When NOT to Use - -**Don't use this when:** -- The caller could just `import` what they need themselves -- You're importing utility functions that aren't part of your module's "DSL" -- The imports create naming conflicts with common functions - -**Over-application example:** -```elixir -defmacro __using__(_opts) do - quote do - import MyApp.Utils # 50+ utility functions dumped into caller - import Enum # Why? Caller can do this themselves - import Map # Polluting namespace with standard lib - end -end -``` - -**Better alternative:** -```elixir -defmacro __using__(_opts) do - quote do - # Only import what THIS module's workflow requires - import MyApp.DSL, only: [field: 2, validate: 1] - end -end -``` - -**Why:** `use` should import the *minimum* needed for the module's intended workflow. If you're importing generic utilities, you're making decisions for the caller that they should make themselves.''', - - 6: ''' -### When to Use - -**Triggers:** -- Multiple modules from the same parent namespace are used together -- Full module paths are making code hard to read -- The aliased modules are used frequently (3+ times in the file) - -**Example — before:** -```elixir -def process(input) do - Phoenix.Router.Route.new(input) - |> Phoenix.Router.Scope.apply_scope(Phoenix.Router.Scope.current()) - |> Phoenix.Router.Helpers.generate() -end -``` - -**Example — after:** -```elixir -alias Phoenix.Router.{Route, Scope, Helpers} - -def process(input) do - Route.new(input) - |> Scope.apply_scope(Scope.current()) - |> Helpers.generate() -end -``` - -### When NOT to Use - -**Don't use this when:** -- A module is referenced only once (inline the full path) -- The alias would be ambiguous (two `Route` modules from different namespaces) -- You're in a test file and the full path makes assertions clearer - -**Over-application example:** -```elixir -# Aliasing a module used exactly once -alias MyApp.Workers.BatchProcessor - -def run do - BatchProcessor.start() # Only reference — alias adds noise -end -``` - -**Better alternative:** -```elixir -def run do - MyApp.Workers.BatchProcessor.start() # One use — full path is fine -end -``` - -**Why:** Aliases trade verbosity for indirection. When a module appears once, the full path is documentation. When it appears many times, the alias is readability. Find the crossover point (typically 2-3 uses).''', - - 7: ''' -### When to Use - -**Triggers:** -- A struct field stores a boolean value -- The field answers a yes/no question about the struct -- You want the field's type to be self-evident without checking typespecs - -**Example — before:** -```elixir -defstruct [:path, :trailing_slash, :verified] -# Is :trailing_slash the slash character? A boolean? The position? -``` - -**Example — after:** -```elixir -defstruct [:path, :trailing_slash?, :verified?] -# Immediately clear these are booleans -``` - -### When NOT to Use - -**Don't use this when:** -- The field isn't a boolean (e.g., `:status` that can be `:active`/`:inactive`) -- You're working with external serialization that can't handle `?` in keys -- The field represents a count, enum, or value rather than a yes/no question - -**Over-application example:** -```elixir -defstruct [:user?, :admin?, :count?] -# :user? — is this "is user present?" or "the user value"? -# :count? — definitely not a boolean -``` - -**Better alternative:** -```elixir -defstruct [:user, :admin?, :count] -# :user is the user struct, :admin? is a boolean, :count is an integer -``` - -**Why:** The `?` suffix should only mark genuine booleans. Using it on non-boolean fields creates confusion about the field's type and breaks the convention's usefulness as a type signal.''', -} - -for i in range(len(parts)): - if i in when_sections: - parts[i] = parts[i].rstrip() + "\n\n" + when_sections[i].strip() - -output = separator.join(parts) + "\n" - -with open("patterns/modules.md", "w") as f: - f.write(output) - -print(f"Done! {len(output)} chars") diff --git a/enhance_modules2.py b/enhance_modules2.py deleted file mode 100644 index 8846b02..0000000 --- a/enhance_modules2.py +++ /dev/null @@ -1,432 +0,0 @@ -#!/usr/bin/env python3 -"""Add When to Use / When NOT to Use sections to modules.md -Part 0 contains preamble + section 1. Parts 1-6 are sections 2-7.""" - -with open("patterns/modules.md", "r") as f: - content = f.read() - -separator = "\n\n---\n\n" -parts = content.split(separator) -assert len(parts) == 7, f"Expected 7 parts, got {len(parts)}" - -# Map: part index -> when section content -# Part 0 = preamble + section 1 -# Part 1 = section 2 -# ... -# Part 6 = section 7 - -when_sections = { - 0: ''' -### When to Use - -**Triggers:** -- Your module has grown beyond ~300 lines with distinct sub-responsibilities -- External code only needs the parent module but implementation is complex -- You find yourself prefixing private functions with a concept name (e.g., `scope_push`, `scope_pop`) - -**Example — before:** -```elixir -# Everything crammed into one flat module -defmodule MyApp.Router do - # 800 lines mixing route compilation, scope tracking, and helper generation - def compile_route(...), do: # ... - def push_scope(...), do: # ... - def pop_scope(...), do: # ... - def generate_helper(...), do: # ... -end -``` - -**Example — after:** -```elixir -# Parent module is the public API -defmodule MyApp.Router do - # Public API delegates to focused submodules - def compile(routes), do: MyApp.Router.Compiler.compile(routes) -end - -# Submodules handle implementation -defmodule MyApp.Router.Compiler do - @moduledoc false - # ... -end - -defmodule MyApp.Router.Scope do - @moduledoc false - # ... -end -``` - -### When NOT to Use - -**Don't use this when:** -- The module is small and cohesive (< 200 lines) -- Nesting would exceed 3 levels (`A.B.C.D` is usually too deep) -- The "submodule" has its own independent public API (make it a sibling instead) - -**Over-application example:** -```elixir -# Over-nesting a simple utility -defmodule MyApp.Utils.String.Formatting.Case do - def upcase(s), do: String.upcase(s) -end -``` - -**Better alternative:** -```elixir -defmodule MyApp.StringUtils do - def upcase(s), do: String.upcase(s) -end -``` - -**Why:** Nesting should reflect genuine conceptual hierarchy. If you're creating submodules for 2-3 functions that don't have independent complexity, you're adding navigational overhead without architectural benefit.''', - - 1: ''' -### When to Use - -**Triggers:** -- You're writing a new module and need to decide function ordering -- A module has grown organically and functions are scattered randomly -- You're reviewing code and finding it hard to locate the public API - -**Example — before:** -```elixir -defmodule UserService do - defp hash_password(pw), do: # ... - - def create(attrs) do - # uses hash_password - end - - def start_link(opts), do: GenServer.start_link(__MODULE__, opts) - - defp validate(attrs), do: # ... - - def get(id), do: # ... -end -``` - -**Example — after:** -```elixir -defmodule UserService do - # Lifecycle - def start_link(opts), do: GenServer.start_link(__MODULE__, opts) - - # Public API - def create(attrs), do: # ... - def get(id), do: # ... - - # Private helpers - defp validate(attrs), do: # ... - defp hash_password(pw), do: # ... -end -``` - -### When NOT to Use - -**Don't use this when:** -- You have a tiny module (< 5 functions) where ordering doesn't matter much -- The module is a pure data module (just a struct + typespec) -- "Logical grouping" puts closely related public+private pairs together for readability - -**Over-application example:** -```elixir -# Forcing start_link to the top in a module that isn't an OTP process -defmodule MyApp.Parser do - # This module has no lifecycle — don't force OTP ordering - def start_link(_), do: raise "not a process" # Just to match the pattern? - def parse(input), do: # ... -end -``` - -**Better alternative:** -```elixir -defmodule MyApp.Parser do - @moduledoc "Parses input format X into structs" - - def parse(input), do: # ... - def parse!(input), do: # ... - - defp tokenize(input), do: # ... -end -``` - -**Why:** The ordering convention exists to make OTP-aware modules predictable. For non-OTP modules, lead with the primary public function (the one callers reach for first) and let the rest follow logically.''', - - 2: ''' -### When to Use - -**Triggers:** -- A module exists purely for internal code organization -- Users of your library should never call this module directly -- The module is a helper that could change or disappear between versions - -**Example — before:** -```elixir -defmodule MyApp.Repo.QueryBuilder do - @moduledoc """ - Builds Ecto queries for the Repo module. - """ - # Now appears in docs, users try to call it directly -end -``` - -**Example — after:** -```elixir -defmodule MyApp.Repo.QueryBuilder do - @moduledoc false - # Hidden from docs, clearly internal -end -``` - -### When NOT to Use - -**Don't use this when:** -- The module is part of your public API (even if rarely used) -- Users need to implement callbacks or extend the module -- The module defines a behaviour or protocol that others implement - -**Over-application example:** -```elixir -# Hiding a module that users actually need -defmodule MyApp.Errors do - @moduledoc false # But users need to pattern-match on these! - - defmodule NotFound do - defexception [:message] - end -end -``` - -**Better alternative:** -```elixir -defmodule MyApp.Errors do - @moduledoc "Error types raised by MyApp operations." - - defmodule NotFound do - @moduledoc "Raised when a resource cannot be found." - defexception [:message] - end -end -``` - -**Why:** `@moduledoc false` means "this is not for you." If users catch your exceptions or match on your structs, they need documentation. Hide implementation details, not public contracts.''', - - 3: ''' -### When to Use - -**Triggers:** -- Your struct has fields that make no sense as `nil` (creating one without them is a bug) -- You're modeling a value object where all fields define its identity -- Incomplete structs would cause confusing runtime errors later - -**Example — before:** -```elixir -defmodule Order do - defstruct [:id, :customer_id, :items, :total] - # Can create %Order{} with everything nil — meaningless -end -``` - -**Example — after:** -```elixir -defmodule Order do - @enforce_keys [:customer_id, :items, :total] - defstruct [:id | @enforce_keys] - # %Order{} without required fields -> immediate compile/runtime error -end -``` - -### When NOT to Use - -**Don't use this when:** -- The struct is built incrementally (e.g., a changeset or builder pattern) -- Most fields have sensible defaults -- The struct represents configuration where partial specs are valid - -**Over-application example:** -```elixir -# Enforcing keys on a struct that's built in stages -defmodule FormState do - @enforce_keys [:step, :name, :email, :address, :payment] - defstruct @enforce_keys - # Can't create a partial form state for step 1! -end -``` - -**Better alternative:** -```elixir -defmodule FormState do - defstruct step: 1, name: nil, email: nil, address: nil, payment: nil - # Built incrementally as user progresses through steps -end -``` - -**Why:** `@enforce_keys` is for structs that represent *complete* values. If your struct represents an evolving state or has legitimate intermediate forms, enforcing all keys makes construction impossible at early stages.''', - - 4: ''' -### When to Use - -**Triggers:** -- Your `use` macro needs to give the caller access to specific functions -- You want to control exactly which functions enter the caller's namespace -- The imported functions are central to the DSL or workflow the module enables - -**Example — before:** -```elixir -defmacro __using__(_opts) do - quote do - # Imports EVERYTHING from three modules — namespace soup - import MyApp.Router.Helpers - import MyApp.Router.Scoping - import MyApp.Router.Compilation - end -end -``` - -**Example — after:** -```elixir -defmacro __using__(_opts) do - quote do - import MyApp.Router, only: [get: 2, post: 2, resources: 2, scope: 2] - import MyApp.Conn, only: [assign: 3, put_status: 2] - end -end -``` - -### When NOT to Use - -**Don't use this when:** -- The caller could just `import` what they need themselves -- You're importing utility functions that aren't part of your module's "DSL" -- The imports create naming conflicts with common functions - -**Over-application example:** -```elixir -defmacro __using__(_opts) do - quote do - import MyApp.Utils # 50+ utility functions dumped into caller - import Enum # Why? Caller can do this themselves - import Map # Polluting namespace with standard lib - end -end -``` - -**Better alternative:** -```elixir -defmacro __using__(_opts) do - quote do - # Only import what THIS module's workflow requires - import MyApp.DSL, only: [field: 2, validate: 1] - end -end -``` - -**Why:** `use` should import the *minimum* needed for the module's intended workflow. If you're importing generic utilities, you're making decisions for the caller that they should make themselves.''', - - 5: ''' -### When to Use - -**Triggers:** -- Multiple modules from the same parent namespace are used together -- Full module paths are making code hard to read -- The aliased modules are used frequently (3+ times in the file) - -**Example — before:** -```elixir -def process(input) do - Phoenix.Router.Route.new(input) - |> Phoenix.Router.Scope.apply_scope(Phoenix.Router.Scope.current()) - |> Phoenix.Router.Helpers.generate() -end -``` - -**Example — after:** -```elixir -alias Phoenix.Router.{Route, Scope, Helpers} - -def process(input) do - Route.new(input) - |> Scope.apply_scope(Scope.current()) - |> Helpers.generate() -end -``` - -### When NOT to Use - -**Don't use this when:** -- A module is referenced only once (inline the full path) -- The alias would be ambiguous (two `Route` modules from different namespaces) -- You're in a test file and the full path makes assertions clearer - -**Over-application example:** -```elixir -# Aliasing a module used exactly once -alias MyApp.Workers.BatchProcessor - -def run do - BatchProcessor.start() # Only reference — alias adds noise -end -``` - -**Better alternative:** -```elixir -def run do - MyApp.Workers.BatchProcessor.start() # One use — full path is fine -end -``` - -**Why:** Aliases trade verbosity for indirection. When a module appears once, the full path is documentation. When it appears many times, the alias is readability. Find the crossover point (typically 2-3 uses).''', - - 6: ''' -### When to Use - -**Triggers:** -- A struct field stores a boolean value -- The field answers a yes/no question about the struct -- You want the field's type to be self-evident without checking typespecs - -**Example — before:** -```elixir -defstruct [:path, :trailing_slash, :verified] -# Is :trailing_slash the slash character? A boolean? The position? -``` - -**Example — after:** -```elixir -defstruct [:path, :trailing_slash?, :verified?] -# Immediately clear these are booleans -``` - -### When NOT to Use - -**Don't use this when:** -- The field isn't a boolean (e.g., `:status` that can be `:active`/`:inactive`) -- You're working with external serialization that can't handle `?` in keys -- The field represents a count, enum, or value rather than a yes/no question - -**Over-application example:** -```elixir -defstruct [:user?, :admin?, :count?] -# :user? — is this "is user present?" or "the user value"? -# :count? — definitely not a boolean -``` - -**Better alternative:** -```elixir -defstruct [:user, :admin?, :count] -# :user is the user struct, :admin? is a boolean, :count is an integer -``` - -**Why:** The `?` suffix should only mark genuine booleans. Using it on non-boolean fields creates confusion about the field's type and breaks the convention's usefulness as a type signal.''', -} - -for i in range(len(parts)): - if i in when_sections: - parts[i] = parts[i].rstrip() + "\n\n" + when_sections[i].strip() - -output = separator.join(parts) + "\n" - -with open("patterns/modules.md", "w") as f: - f.write(output) - -print(f"Done! {len(output)} chars")