ash-project / spark

Tooling for building DSLs in Elixir
MIT License
105 stars 23 forks source link

Functions as parameters #32

Closed fuelen closed 1 year ago

fuelen commented 1 year ago

Describe the bug In some cases, it is not possible to pass a function as a value.

To Reproduce Let's say we have an entity with optional map parameter which accepts functions with arity 1:

defmodule Parameter do
  defstruct [:name, :map]

  @schema [
    name: [
      type: :atom,
      required: true,
      doc: "A name of the parameter"
    ],
    map: [type: {:fun, 1}]
  ]

  def schema, do: @schema
end

This works:

param :id, map: &Function.identity/1

this doesn't:

param :id, map: fn value -> value end

error:

** (ArgumentError) cannot inject attribute @spark_dsl_config into function/macro because cannot escape #Function<2.90808913/1 in :elixir_compiler_6.__MODULE__/1>. The supported values are: lists, tuples, maps, atoms, numbers, bitstrings, PIDs and remote functions in the format &Mod.fun/arity
    (elixir 1.14.3) lib/kernel.ex:3543: Kernel.do_at/5
    (elixir 1.14.3) expanding macro: Kernel.@/1

if I rewrite this using do/end blocks, then it works:

param :id do
  map(fn value -> value end)
end

this doesn't:

param :id do
  map(& &1)
end

error (arity is detected incorrectly):

** (Spark.Error.DslError) [TestSchema]
 commands -> param -> id:
  invalid value for :map option: expected function of arity 1, got: function of arity 0

this doesn't work as well, but with different error:

param :id do
  map(& &1.id)
end

error:

** (ArgumentError) cannot inject attribute @spark_dsl_config into function/macro because cannot escape #Function<2.8108167/1 in :elixir_compiler_2.__MODULE__/1>. The supported values are: lists, tuples, maps, atoms, numbers, bitstrings, PIDs and remote functions in the format &Mod.fun/arity
    (elixir 1.14.3) lib/kernel.ex:3543: Kernel.do_at/5
    (elixir 1.14.3) expanding macro: Kernel.@/1

but this works

param :id do
  map(& Map.get(&1, :id))
end

Expected behavior It would be great if all cases above work, especially one-liners like param :id, map: & &1.id

Runtime

fuelen commented 1 year ago

also, if I forget to type a map key:

param :id, &to_string/1

then I receive a cryptic error:

== Compilation error in file lib/test_schema.ex ==
** (Protocol.UndefinedError) protocol Enumerable not implemented for {:&, [line: 6], [{:/, [line: 6], [{:to_string, [line: 6], nil}, 1]}]} of type Tuple
    (elixir 1.14.3) lib/enum.ex:1: Enumerable.impl_for!/1
    (elixir 1.14.3) lib/enum.ex:166: Enumerable.reduce/3
    (elixir 1.14.3) lib/enum.ex:4307: Enum.map/2
zachdaniel commented 1 year ago

These should all be handleable things. Thanks for the report :) I'll look into it soon.

fuelen commented 1 year ago

functions like & &1 and & &1.id still don't work. Seems like because the line is hardcoded [line: 113]

zachdaniel commented 1 year ago

Oops :) Sorry about that.

zachdaniel commented 1 year ago

I was testing and left that in.

zachdaniel commented 1 year ago

Fixed :)

fuelen commented 1 year ago

Cool! Thank you for quick fixes :)

zachdaniel commented 1 year ago

My pleasure :D