-
Notifications
You must be signed in to change notification settings - Fork 28
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Discuss how to approach macro-heavy libraries #276
Comments
I'm currently working on a proof of concept implementation of a Entire idea is that instead of adding macros to modify Elchemy AST (which would just end up being a type-safety hazardous mess) you get a toolset to create new modules manipulating Elixir AST. So that from user-point of view calling module SomeTestSuite exposing (suite)
import Elchemy.ExUnitPlugin
suite =
-- This will create a module using ExUnit.Case
ExUnitPlugin.suite {someField = "mycontext"} -- <- this is a starting context passed further
-- This will tell it to define a setup inside
|> ExUnitPlugin.setup (\ {someField} -> {ctx = someField} )
-- This will tell it to define a test inside
|> ExUnitPlugin.test "1 + 1 equals 2" (\context -> 1 + 1 == 2 ) Which could then be used normally wherever desired defmodule MyProjectTest do
use ExUnit.Case
SomeTestSuite.suite()
end Which is already proven to work Under the hood however it needs to create a module and INJECT an anonymous function into it's body. defmodule SomeTestSuite do
# It's actually using `Module.create` but using defmodule for better readability here
def suite() do
defmodule ExUnitPlugin.H(ContentHash) do
use ExUnit Case
test "2 + 2 equals 1", context do
assert unquote(f).()
end
end
end
The entire problem however, is how to smuggle the anonymous function inside the created module. Reading on Elixir forum I found this (which I wasn't aware of before)
But I wasn't able to find a way to call such a function. @OvermindDL1 Can you elaborate on how that works and if it's possible to call an anonymous function just by having it's value passed (somehow) to the nested module? |
Ok. Found :erlang.fun_info(Test.test)
[
pid: #PID<0.98.0>,
module: Test,
new_index: 0,
new_uniq: <<64, 160, 30, 242, 89, 33, 1, 230, 243, 33, 207, 54, 132, 229, 36,
142>>,
index: 0,
uniq: 33882359,
name: :"-test/0-fun-0-",
arity: 1,
env: [],
type: :local
] |
Hmm. It might be no use after all. The functions are obviously local so I don't think it's possible to pass them further |
Heh, you can pass them around just fine. An anonymous function is just, essentially internally, a tuple of the module name, the function index in the module function array, and a set of bindings to pass in to it as it's environment (which become part of its called arguments). To 'pass it in', if you don't need an environment, is easy, you can just remotely call it, otherwise maybe unwrap the environment in to it. However, I'm not quite understanding what you are trying to do, let me read in more detail... Are you talking about the |
And don't forget, even macro-heavy libraries are still just only function calls, everything is function calls. :-) If you approach it more like Lisp instead of metaprogramming like in other languages, then that is much closer to how Elixir really is. :-) |
@OvermindDL1 Obviously. But I want to give a [relatively] simple tool for people from outside to be able to write wrappers for common libraries like they would normally do it. Test name body ->
Application (atom "test")
[ Value (String name)
, Variable "context"
, Do
-- A Quotation here is a place we will paste our function in
[ dotApplication Quotation [ Variable "context" ]
]
]
|> unquote body Instead of having to understand all of the internals of how ExUnit works and just do all of the macro work manually. |
What I would want to do is to be able to build AST "around" anonymous functions. So for instance with a simpler example than ExUnit. So that would resolve to something like defmodule MyCallingModule do
def behaviour_implementation(f_to_call) do
defmodule Whatever do
@behaviour MyBehaviour
def some_callback() do
# Here somehow we need to call any function inside our parent, but since in Elm a global function and a local function are virtually the same I need to be able to pass an anonymous function here
f_to_call.()
end
end
end
end |
@OvermindDL1 What do you mean remotely call it? Can you give an example of how to serialize (into AST) an anonymous function and to be able to execute it after rebuilding from serialized information about it? |
What format is the anonymous function in, is it the AST of it, like If you have no environment that needs to be included then just pack it as, say, |
@OvermindDL1 ExUnitPlugin.test (\_ -> 10 + 1 == 11) it might as well be let
f = (\_ -> 10 + 1 == 11)
in
ExUnitPlugin.test f So quoting it on call inside Elchemy isn't really an option. Especially that it wouldn't behave consistent with other functions due to "lazyness" of macros. The problem with using defmodule Test do
def myfunction(), do: fn x -> x + 1
end Even though I can get the name of the anonymous function by doing Test.myfunction() |> :erlang.fun_info()
[
pid: #PID<0.98.0>,
module: Test,
new_index: 0,
new_uniq: <<64, 160, 30, 242, 89, 33, 1, 230, 243, 33, 207, 54, 132, 229, 36,
142>>,
index: 0,
uniq: 33882359,
name: :"-test/0-fun-0-",
arity: 1,
env: [],
type: :local
] That would still make this function local so I couldn't just call it with apply(Test, :"-test/0-fun-0-", [1]) |
Actually if it might be an anon function or it might be a binding to an anon function, there is an easy way to handle that. :-) # All of these do the same thing:
apply(fn -> 42 end, []) # returns 42
f = fn -> 42 end
apply(f, []) # returns 42
defmodule SomewhereElse do
def bloop(), do: 42
end
apply(&SomewhereElse.bloop/0, []) # returns 42 Apply is an 'indirect' call, not super fast (but doesn't matter most of time). |
And yes, the |
@OvermindDL1 I don't think we're on the same page :-) What I need is to be able to call an anonymous function inside of a dynamically declared module, but a function passed from the outside of this module, but also not as an argument but a compile time (which is fortunately run-time too, because it's dynamically declared module) Example defmodule OuterScope do
def declare_module(my_fun) do
defmodule InnerScope do
def execute(), do: my_fun.()
end
end
end |
But the module inside is a quoted expression. And you can't pass anonymous functions to quoted expressions |
Hmm, that code I don't think will do what you expect, you can't really declare a module inside a function, and doing so will work when the compiler exists but will fail in releases. |
@OvermindDL1 I guess that kills my proof of concept here. Have you got any other idea on how to use module level macros without polluting the scope of the modules? For instance how one might go about writing an Elchemy wrapper for this: defmodule Hello.User do
use Ecto.Schema
schema "users" do
field :bio, :string
field :email, :string
field :name, :string
field :number_of_pets, :integer
timestamps()
end
end I thought that defining dynamically a module implementing those might be the best idea, but now I see it's probably not. |
I'm starting to slowly reconsider returning the |
I'd probably just keep them as function calls. I don't think that elm has top-level calling like better languages like OCaml and such, so let's give the 'top level' calling scope a special name, how about __top__ =
let schema = remote("Ecto.Schema.schema", 2) in
let field = remote("Ecto.Schema.field", 2) in
schema "users" {do: [
field Bio String,
field Email String,
field Name String,
field Number_of_pets Integer,
remote("Ecto.Schema.timestamps", 0)
]} Or whatever you want, the important bit is not necessarily treating it as calls directly, but recognizing that the AST is what it is important to Macro's, and a |
@OvermindDL1 |
The |
/me still thinks wende should instead compile OCaml down to Elixir instead... almost identical syntax, but has 'more' and far far better designed |
@OvermindDL1 First of all - sunk cost fallacy 😉 |
OCaml is hard to read? I'd love for you to show examples in Elm that I could translate into OCaml that is hard to read. ;-) Ah but lowercase types matches Elixir/Erlang too! ;-) |
@OvermindDL1 Honestly it might be just me not being used to the syntax. Plus I don't want to get into competition with guys from Alpaca Lang. They're doing a great job |
Remember, Elm's syntax was based on OCaml's, in fact it is so close to a direct translation that changing a couple
Heh, good luck there, I've seen how extremely hostile they are to suggestions (without unemotional reasoning as to their stances), it very much reminds me of the hostile and antagonistic Haskell community...
A couple things though:
As well as OCaml has the fastest optimizing compiler of any compiled language in existence. :-) |
As well as OCaml is designed to be extended. It even has 2 backends to compile to javascript (one is more readable, one is lower level but more powerful). So you could use the same language for the BEAM, javascript, and native compilation. ;-) |
Oh yes. Trust me, I'm aware
My impression was always the exact opposite. Before Reason came out I felt incredibly intimidated by OCaml's errors and general tooling. Definitely not less than Haskells though, that's true. It's been a long time since I last tried it. Might be I should give it another try sometime |
Reason has to date not created anything 'new', but has packaged some well known things together, like It doesn't include the indenter of OCaml has a couple build systems, ocamlbuild is the usual 'default' old one, but most people use JBuilder as it's just better, and in fact so many people use JBuilder that the next OCaml version is having it be the default build system under a new name of Dune.
If you aren't on windows it's trivial to setup and install (I just And all of these tools it has have existed for years. ^.^ |
So after experimenting with it a little there's a summary of pitfalls using a top/meta function: Meta definition resolving to code injected into the top scopePitfalls:
meta =
something myfun -- error
myfun = 1 And you can't because something(myfun()) # No such thing as myfun yet
defp myfun(), do: 1
And that feels really bad, unless explicitly explained but that's an additional layer of complexity I'd really prefer to avoid. A good example on how to present it would be a PoC showing a test suite using ExUnit, with tests (most elegantly generated from an existing elm suite. Which would mean a simple and clean non-compiler-solution to turning this module UnitTests exposing (..)
import Test exposing (..)
import Expect
all : Test
all =
describe "Test"
[ test "Test truth" <|
\() ->
Expect.equal "a" "a"
] and this module MyAppTest do
use ExUnit.Case
UnitTests.all()
end Into something that behaves like this: module MyAppTest do
use ExUnit.Case
describe "Test"
test "Test truth" do
assert "a" == "a"
end
end
end Because the first solution that comes to my mind would be: module UnitTests exposing (..)
import Test exposing (..)
import Expect
meta =
all |> turnToMacro
all : Test
all =
describe "Test"
[ test "Test truth" <|
\() ->
Expect.equal "a" "a"
] But this won't work because |
Much closer to the solution, however there is one caveat of readability: b = a + 1
function(a + 1) == function(b) We can be sure the output will be always true, however
Here we cannot. Because That leads to a problem that:
would work, while
wouldn't. The only solution I can think of is introducing a new keyword/type/function to Elchemy that would explicitly denote passing the code as an AST rather than it's value (basically what It could be for example
It's not yet evaluated, but passed as as an AST This way we could know straigh away that function_or_macro(a + 1) -- This is a function
function_or_macro(Do <| a + 1) -- this is a macro The problem is it introduces a certain layer of complexity that I'd like to avoid. |
Instead of a |
* Plugin tests work * Macro system working * Plugins solved in core * Plugins done in elchemy-core * resolve full functions arity * Better vars in meta and mod when crashing * core * core change * core change test * fix test of test-project * better .gitignore * better .gitignore in core * fix bump * 0.7.0-0 * 0.7.0-0 / 2018-05-11 ================ * fix bump * better .gitignore in core * better .gitignore * fix test of test-project * core change test * core change * core * Better vars in meta and mod when crashing * resolve full functions arity * Plugins done in elchemy-core * Plugins solved in core * Macro system working * Plugin tests work * release from wherever * 0.7.0-1 * 0.7.0-2 * 0.7.0-2 / 2018-05-11 ================ * 0.7.0-1 * release from wherever * Proper dev release * remove docs * Core update to 7.0.1 * 0.7.0 * 0.7.0 / 2018-05-14 ================ * Core update to 7.0.1 * remove docs * Proper dev release * remove docs * Configurable elchemy path exec for dev mode * strip meta as a helper in core * 0.7.1 * 0.7.1 / 2018-05-15 ================ * remove docs * Remove restore docs
================ * Closes #276 macro plugins (#331) * Update elm-package.json * Bug: Closes #327 (#328) * Closes #329 - Warn when elchemy not installed during project compilation (#330) * Closes #238 - Debug.log print in Elchemy format instead of Elixir format (#325) * Closes #322 Warn when elchemy line put incorrectly (#323) * Update README.md (#320) * correct typos (#318) * type savety and Elixir (#310) * Closes #313 Fixes interop in gitbook and new gitbbok (#317) * Corrected typo, set Compiler to 90% (#314) * Corrected typo (#306) * Corrected typos (#307) * correct typo (#308) * Delete FEATURES.MD * Create CODE_OF_CONDUCT.md (#304) * Typo in gitbook (#305) * 0.6.6 / 2018-03-05 ================ * 0.6.6 * format properly * 0.6.5 / 2018-03-05 ================ * 0.6.5 * format when > 1.6.0 (#300) * Update README.md * Better references in maturity of the project summary * Update SIDE_EFFECTS.md * Update SUMMARY.md * Update SUMMARY.md * Add SYNTAX OVERVIEW * Create SYNTAX.md * Fix to 292 (#293) * Closes #290 - incremental compiler bug (#291) * 0.6.4 / 2018-02-07 ================ * 0.6.4 * Updated core
================ * Clean makes init not able to work (#341) * No project name (#339) * Correct typos in roadmap/STRUCTURES.md (#319) * Added formating and test (#337) * 0.7.2 / 2018-05-16 ================ * 0.7.2 * Closes #276 macro plugins (#331) * Update elm-package.json * Bug: Closes #327 (#328) * Closes #329 - Warn when elchemy not installed during project compilation (#330) * Closes #238 - Debug.log print in Elchemy format instead of Elixir format (#325) * Closes #322 Warn when elchemy line put incorrectly (#323) * Update README.md (#320) * correct typos (#318) * type savety and Elixir (#310) * Closes #313 Fixes interop in gitbook and new gitbbok (#317) * Corrected typo, set Compiler to 90% (#314) * Corrected typo (#306) * Corrected typos (#307) * correct typo (#308) * Delete FEATURES.MD * Create CODE_OF_CONDUCT.md (#304) * Typo in gitbook (#305) * 0.6.6 / 2018-03-05 ================ * 0.6.6 * format properly * 0.6.5 / 2018-03-05 ================ * 0.6.5 * format when > 1.6.0 (#300) * Update README.md * Better references in maturity of the project summary * Update SIDE_EFFECTS.md * Update SUMMARY.md * Update SUMMARY.md * Add SYNTAX OVERVIEW * Create SYNTAX.md * Fix to 292 (#293) * Closes #290 - incremental compiler bug (#291) * 0.6.4 / 2018-02-07 ================ * 0.6.4 * Updated core
In Elixir a lot of libraries rely on macros and while these used inside functions can be easily mimicked and wrapped, when it comes to those as top scope definitions (right inside modules f.i) it starts to become quite unwieldy
One of the simplest examples is how to write a test. Let's say we've got a standard ExUnit.Case macro.
Right now there is no way to express that in Elchemy.
And while it is possible to do some tricks like
While this is correct code that would work i'd say it's quite far from what we would call an elegant solution.
This issue's purpose is to discuss all the possibilities and come out with something most that solves most problems without causing too much over-complication
The text was updated successfully, but these errors were encountered: