DarkKernel - elixir functions which use dark magic

Some initial reactions to this syntax:

  • the = inside the brackets feels strange, but that may be because it makes me think of Ruby’s default argument syntax.

  • having a d in the “pattern” but then not failing to match feels odd compared to matching the literal forms where those keys are required to match

A less vibes-based issue I encountered trying this out is that it fails if a default expression calls a function with two arguments:

iex(3)> opts = [a: 1, b: 2]
[a: 1, b: 2]
iex(4)> ~k{a, b, c: Integer.pow(2, 4) = opts}
** (DarkKernel.DarkKeywords.InvalidKeywordError) Invalid keyword: 4)
Keywords must be valid Elixir variable names and contain only ASCII characters.
They can be preceded by a bang (e.g. `!key`) or contain a default value (e.g. `key: default`).

    (dark_kernel 0.1.0) lib/dark_kernel/dark_keywords.ex:116: anonymous fn/2 in DarkKernel.DarkKeywords.parse_keyword_match/1
    (elixir 1.14.0) lib/enum.ex:2468: Enum."-reduce/3-lists^foldl/2-0-"/3
    (dark_kernel 0.1.0) lib/dark_kernel/dark_keywords.ex:103: DarkKernel.DarkKeywords.parse_keyword_match/1
    (dark_kernel 0.1.0) lib/dark_kernel/dark_keywords.ex:193: DarkKernel.DarkKeywords.do_match_keywords/1
    (dark_kernel 0.1.0) expanding macro: DarkKernel.sigil_k/2
    iex:4: (file)

or if a default argument is a literal with a comma in it:

iex(4)> ~k{a, b, c: "Hello, world!" = opts}
** (DarkKernel.DarkKeywords.InvalidKeywordError) Invalid keyword: world!"
Keywords must be valid Elixir variable names and contain only ASCII characters.
They can be preceded by a bang (e.g. `!key`) or contain a default value (e.g. `key: default`).

    (dark_kernel 0.1.0) lib/dark_kernel/dark_keywords.ex:116: anonymous fn/2 in DarkKernel.DarkKeywords.parse_keyword_match/1
    (elixir 1.14.0) lib/enum.ex:2468: Enum."-reduce/3-lists^foldl/2-0-"/3
    (dark_kernel 0.1.0) lib/dark_kernel/dark_keywords.ex:103: DarkKernel.DarkKeywords.parse_keyword_match/1
    (dark_kernel 0.1.0) lib/dark_kernel/dark_keywords.ex:193: DarkKernel.DarkKeywords.do_match_keywords/1
    (dark_kernel 0.1.0) expanding macro: DarkKernel.sigil_k/2
    iex:4: (file)

In at least these two cases, Code.string_to_quoted! could be helpful if you wrap the whole expression passed to ~k in [ ]:

iex(5)> Code.string_to_quoted!("[a, b, c: \"Hello, world!\" = opts]")
[
  {:a, [line: 1], nil},
  {:b, [line: 1], nil},
  {:c, {:=, [line: 1], ["Hello, world!", {:opts, [line: 1], nil}]}}
]

iex(6)> Code.string_to_quoted!("[a, b, c: Integer.pow(2, 4) = opts]")
[
  {:a, [line: 1], nil},
  {:b, [line: 1], nil},
  {:c,
   {:=, [line: 1],
    [
      {{:., [line: 1], [{:__aliases__, [line: 1], [:Integer]}, :pow]},
       [line: 1], [2, 4]},
      {:opts, [line: 1], nil}
    ]}}
]

A little (ok, probably LOTS) of pattern matching should be able to split / transform these into the output AST. It could even support cases like two equals signs that currently raise an exception:

iex(8)> Code.string_to_quoted!("[!a, b: (_ = 1 + 2) = opts]")      
[
  {:!, [line: 1], [{:a, [line: 1], nil}]},
  {:b,
   {:=, [line: 1],
    [
      {:=, [line: 1], [{:_, [line: 1], nil}, {:+, [line: 1], [1, 2]}]},
      {:opts, [line: 1], nil}
    ]}}
]
3 Likes