diff --git a/priv/blog/staged/2023/2023-05-11-There-are-macros-and-there-are-macros.md b/priv/blog/staged/2023/2023-05-11-There-are-macros-and-there-are-macros.md index 22d7ad6..db29f5f 100644 --- a/priv/blog/staged/2023/2023-05-11-There-are-macros-and-there-are-macros.md +++ b/priv/blog/staged/2023/2023-05-11-There-are-macros-and-there-are-macros.md @@ -1,26 +1,27 @@ --- -published_at: '2023-05-11 20:00:29.789858Z' -state: 'published' +published_at: "2023-05-11 20:00:29.789858Z" +state: "published" past_slugs: [] -slug: 'there-are-macros' -title: 'There are macros and there are *macros*' -created_at: '2023-05-11 19:39:18.027487Z' -id: '176fb334-6f5c-4ed8-bb16-54b4359eafa0' -tag_line: 'Not all macros are built the same.' -tag_names: - - 'elixir' - - 'ash' - - 'macros' -author: 'Zach Daniel' -body_html: '
__ash_blog_newline_hack__ A common criticism of Ash is that it has too many macros. This is an understandable position to take, but I think its important to distinguish between two main kinds of macros. One of them should be used extremely sparingly, and the other I think its okay to use a bit more liberally. We’ll call them “configuration macros” and “metaprogramming macros”. Keep in mind these names are a bit made up and the boundary between the two is not always cut and dry. When people talk about “magic” in the context of macros, they are mostly talking about “metaprogramming macros”. Lets take a look at some examples.__ash_blog_newline_hack__
__ash_blog_newline_hack____ash_blog_newline_hack__ This snippet is from__ash_blog_newline_hack__ __ash_blog_newline_hack__ NX__ash_blog_newline_hack__ __ash_blog_newline_hack__ . This function can run as regular elixir code, or it can be compiled to run using multiple backends meant for numerical processing, including running on the GPU. The code that is actually compiled under the hood looks__ash_blog_newline_hack__ __ash_blog_newline_hack__ dramatically__ash_blog_newline_hack__ __ash_blog_newline_hack__ different from what you see in front of you. This is not a bad thing! If you had to use or write the actual underlying code, you’d get nothing done. Check out Nx’s documentation for more.__ash_blog_newline_hack__
__ash_blog_newline_hack__defn softmax(t) do__ash_blog_newline_hack__ Nx.exp(t) / Nx.sum(Nx.exp(t))__ash_blog_newline_hack__end
__ash_blog_newline_hack__ Here is an example from__ash_blog_newline_hack__Nebulex
, an ETS based caching library. In this example, the metaprogramming is quite hidden from you. The actual logic happens in a__ash_blog_newline_hack__ __ash_blog_newline_hack__ __ash_blog_newline_hack__ compiler callback__ash_blog_newline_hack__ __ash_blog_newline_hack__ __ash_blog_newline_hack__ . It involves rewriting the functions to intercept calls and decide if the cached version should be used, or if the value should be computed and cached. This kind of macro is quite magical given how indirect it is, but at the same time in applications where cached functions are common, it stops feeling like magic and starts feeling like a language feature. If I was looking at a large code-base that used this once, I’d likely be concerned. The mental overhead of understanding it for one single case likely isn’t worth it.__ash_blog_newline_hack__
defmodule MyApp.Example do__ash_blog_newline_hack__ use Nebulex.Caching__ash_blog_newline_hack____ash_blog_newline_hack__ @decorate cacheable(cache: MyApp.Cache, key: :cache_key)__ash_blog_newline_hack__ def get_by_name(name, age) do__ash_blog_newline_hack__ ...__ash_blog_newline_hack__ end__ash_blog_newline_hack__end
__ash_blog_newline_hack__ This one is from__ash_blog_newline_hack__Ash
. We do similar magic to the__ash_blog_newline_hack__Nx
example from above for this. Specifically, we have an expression that can be run natively to a data layer (i.e__ash_blog_newline_hack__SQL
) or in elixir. And Ash reserves the right to decide where it gets run, depending on the context and requirements.__ash_blog_newline_hack__
calculate :full_name, :string, expr(first_name <> " " <> last_name)
__ash_blog_newline_hack__ As you can see, all of these examples represent a significant amount of complexity boiled down to very simple macros. This will also be true of the upcoming examples of “configuration macros”, but the big difference here is that each macro call represents very complex code being executed on your behalf.__ash_blog_newline_hack__
__ash_blog_newline_hack____ash_blog_newline_hack__ Lets take a look at some examples of macros that I would consider__ash_blog_newline_hack__ __ash_blog_newline_hack__ less__ash_blog_newline_hack__ __ash_blog_newline_hack__ magical.__ash_blog_newline_hack__
__ash_blog_newline_hack____ash_blog_newline_hack__ This one is likely familiar to most people who have worked with Elixir professionally. Here we have an Ecto.Schema. In this example we actually have three separate macros. The__ash_blog_newline_hack__schema/2
macro sets up some module attributes, and__ash_blog_newline_hack__field/2
adds to those module attributes.__ash_blog_newline_hack__timestamps/0
sets up the schema to track__ash_blog_newline_hack__inserted_at
and__ash_blog_newline_hack__updated_at
timestamps. On its own, however, an__ash_blog_newline_hack__Ecto.Schema
doesn’t really__ash_blog_newline_hack__ __ash_blog_newline_hack__ do__ash_blog_newline_hack__ __ash_blog_newline_hack__ anything. There also isn’t a bunch of code hidden behind the scenes that you need to be aware of being executed. This sets up some information on the module that can be read back later. For example:__ash_blog_newline_hack__MyApp.MySchema.__schema__(:fields)
would return__ash_blog_newline_hack__[:field_name, :inserted_at, :updated_at]
in this case. This is a classic example of what I’d call a “configuration macro”.__ash_blog_newline_hack__
defmodule MyApp.MySchema do__ash_blog_newline_hack__ schema "table" do__ash_blog_newline_hack__ field :field_name, :type__ash_blog_newline_hack____ash_blog_newline_hack__ timestamps()__ash_blog_newline_hack__ end__ash_blog_newline_hack__end
__ash_blog_newline_hack__ Here we can see an example from__ash_blog_newline_hack__Absinthe
‘s documentation. Absinthe is a tool for building GraphQL apis with Elixir. This one slightly straddles the line between “configuration macros” and “metaprogramming macros”, but I think still lands on the side of a “configuration macro”. We configure a field in our GraphQL API, and how to resolve it. There are quite a few macros involved in building an API with__ash_blog_newline_hack__Absinthe
. Here we configure what code gets run to resolve a given field, but ultimately the responsibility for what code runs is our own.__ash_blog_newline_hack__
defmodule BlogWeb.Schema do__ash_blog_newline_hack__ use Absinthe.Schema__ash_blog_newline_hack__ ...__ash_blog_newline_hack____ash_blog_newline_hack__ query do__ash_blog_newline_hack__ @desc "Get all posts"__ash_blog_newline_hack__ field :posts, list_of(:post) do__ash_blog_newline_hack__ resolve &Resolvers.Content.list_posts/3__ash_blog_newline_hack__ end__ash_blog_newline_hack__ end__ash_blog_newline_hack__end
__ash_blog_newline_hack__ Finally, lets take a look at some code from an__ash_blog_newline_hack__Ash.Resource
. I’ll choose a relatively complex example, specifically the defining of an “action”.__ash_blog_newline_hack__
create :create do__ash_blog_newline_hack__ primary? true__ash_blog_newline_hack____ash_blog_newline_hack__ accept [:text]__ash_blog_newline_hack____ash_blog_newline_hack__ argument :public, :boolean do__ash_blog_newline_hack__ allow_nil? false__ash_blog_newline_hack__ default true__ash_blog_newline_hack__ end__ash_blog_newline_hack____ash_blog_newline_hack__ change fn changeset, _ ->__ash_blog_newline_hack__ if Ash.Changeset.get_argument(changeset, :public) do__ash_blog_newline_hack__ Ash.Changeset.force_change_attribute(changeset, :visibility, :public)__ash_blog_newline_hack__ else__ash_blog_newline_hack__ changeset__ash_blog_newline_hack__ end__ash_blog_newline_hack__ end__ash_blog_newline_hack____ash_blog_newline_hack__ change relate_actor(:author)__ash_blog_newline_hack__end
__ash_blog_newline_hack__ For those not familiar with Ash, lets break it down.__ash_blog_newline_hack__
__ash_blog_newline_hack____ash_blog_newline_hack__primary? true
means that if something wants to create one of these, this is the action it should call by default. This is used by certain internal actions and can be used by you or other extensions to determine which action to call in lieu of an explicitly configured action.__ash_blog_newline_hack__
__ash_blog_newline_hack__accept [:text]
means that you can set the__ash_blog_newline_hack__text
attribute when calling this action.__ash_blog_newline_hack__
__ash_blog_newline_hack__argument :public, :boolean, …
adds an additional input to the action that doesn’t map to an attribute, called__ash_blog_newline_hack__:public
that must have a value, but defaults to__ash_blog_newline_hack__true
.__ash_blog_newline_hack__
__ash_blog_newline_hack__ The anonymous function you see with__ash_blog_newline_hack__change fn changeset, _ ->
is what we call an “inline change”. A__ash_blog_newline_hack__change
is a function that takes a changes and returns a changeset. If you’ve worked with Phoenix or with Plug, you’ll recognize this pattern, it works effectively the same as__ash_blog_newline_hack__Plug
, working on a__ash_blog_newline_hack__Conn
. Except in this case, its a__ash_blog_newline_hack__change
working on an__ash_blog_newline_hack__Ash.Changeset
. We use this argument to map a boolean toggle input to an enumerated attribute.__ash_blog_newline_hack__change relate_actor(:author)
refers to a “built in change”. This change is provided by Ash out of the box, and this relates the actor (A.K.A current user) to the thing that was just created as the__ash_blog_newline_hack__:author
.__ash_blog_newline_hack__
__ash_blog_newline_hack__ These are all macros! However, they again map to an introspectable structure, acting more as configuration than as a traditional macro.__ash_blog_newline_hack__
__ash_blog_newline_hack__iex(1)> Ash.Resource.Info.action(Twitter.Tweets.Tweet, :create)__ash_blog_newline_hack__%Ash.Resource.Actions.Create{__ash_blog_newline_hack__ name: :create,__ash_blog_newline_hack__ primary?: true,__ash_blog_newline_hack__ type: :create,__ash_blog_newline_hack__ …__ash_blog_newline_hack__ arguments: [__ash_blog_newline_hack__ %Ash.Resource.Actions.Argument{__ash_blog_newline_hack__ name: :public,__ash_blog_newline_hack__ …__ash_blog_newline_hack__ }__ash_blog_newline_hack__ ],__ash_blog_newline_hack__ changes: [__ash_blog_newline_hack__ %Ash.Resource.Change{...},__ash_blog_newline_hack__ %Ash.Resource.Change{__ash_blog_newline_hack__ change: {Ash.Resource.Change.RelateActor,__ash_blog_newline_hack__ [allow_nil?: false, relationship: :author]},__ash_blog_newline_hack__ …__ash_blog_newline_hack__ }__ash_blog_newline_hack__ ],__ash_blog_newline_hack__ …__ash_blog_newline_hack__}
__ash_blog_newline_hack__ This inversion of control is something we’re familiar with in other tools like__ash_blog_newline_hack__Plug
or__ash_blog_newline_hack__Phoenix.Endpoint/Router
. They are great examples of how this pattern can allow for code reuse, and make complex chains of behavior easier to reason about.__ash_blog_newline_hack__
__ash_blog_newline_hack__ Above I’ve made a case for macros not necessarily being what gives Ash its relatively steep learning curve. We’ve also create a large suite of tools to mitigate the difficulties that these configuration macros can have. Take a look at__ash_blog_newline_hack__ __ash_blog_newline_hack__ https://hex.pm/packages/spark__ash_blog_newline_hack__ __ash_blog_newline_hack__ for more information on those tools.__ash_blog_newline_hack__
__ash_blog_newline_hack____ash_blog_newline_hack__ With that said, in that one example above, we had to add five new words to our vocabulary. This is what can make Ash difficult. When you set out to write your own application patterns, you can let your own vocabulary evolve. This provides a natural learning curve. I’d also argue that it gives you tons of opportunities to repeat the mistakes of every developer who went through that same process.__ash_blog_newline_hack__
__ash_blog_newline_hack____ash_blog_newline_hack__ Ash presents a huge suite of integrated tools, a suite that gets bigger and bigger every day and, in order to leverage it, you need to learn a whole new way of doing things. This is a very valid reason not to use Ash. Our biggest proponents will tell you that it’s worth it to put in the effort to learn these tools. As would I. And I don’t mean for the idea of what the framework may become, but for the benefits you can get right now.__ash_blog_newline_hack__
__ash_blog_newline_hack____ash_blog_newline_hack__ With that said, I think a diversity of mindsets is the most important thing that we can foster as a community, and there is a fine line between telling people “Hey, I think we’ve got some good ideas over here, I think you should check them out”, and saying “Hey, this is the right way to do things, stop doing things your way and do them our way”. So while I’m of course a proponent of Ash, my plan is not to__ash_blog_newline_hack__ __ash_blog_newline_hack__ talk anyone into using it__ash_blog_newline_hack__ __ash_blog_newline_hack__ . I have one simple goal, which is to continue to expand this integrated suite of tools, adding new capabilities that are only possible for things built in this way. Ash is a snowball, rolling down a hill, and it has barely even begun to gather snow. By doing this, we move the threshold by which cost of learning the “Ash way” is offset by the benefits. For many users (more every day) that threshold has already been crossed. For others, it may never be crossed, and thats okay 😊. If the tools we build can help even one person find success on their Elixir journey, then I’m a happy camper.__ash_blog_newline_hack__
__ash_blog_newline_hack____ash_blog_newline_hack__ The cost of learning something like Ash is not trivial. It depends on you, your problem space, your experience level, and many other factors as to whether or not that juice is worth the squeeze. My goal, and the core team’s goal, is to continue to provide leverage for those already using Ash. To take the patterns we’ve set down and take them even further, increasing the value of our existing users’ investments. Partially because we count among those users, but also because we believe in what we’re building and its ability to help us and others build bigger and better software.__ash_blog_newline_hack__
' -inserted_at: '2023-05-11 19:39:18.027487Z' -updated_at: '2023-05-11 20:00:29.800042Z' +slug: "there-are-macros" +title: "There are macros and there are *macros*" +created_at: "2023-05-11 19:39:18.027487Z" +id: "176fb334-6f5c-4ed8-bb16-54b4359eafa0" +tag_line: "Not all macros are built the same." +tag_names: + - "elixir" + - "ash" + - "macros" +author: "Zach Daniel" +body_html: '__ash_blog_newline_hack__ A common criticism of Ash is that it has too many macros. This is an understandable position to take, but I think its important to distinguish between two main kinds of macros. One of them should be used extremely sparingly, and the other I think its okay to use a bit more liberally. We’ll call them “configuration macros” and “metaprogramming macros”. Keep in mind these names are a bit made up and the boundary between the two is not always cut and dry. When people talk about “magic” in the context of macros, they are mostly talking about “metaprogramming macros”. Lets take a look at some examples.__ash_blog_newline_hack__
__ash_blog_newline_hack____ash_blog_newline_hack__ This snippet is from__ash_blog_newline_hack__ __ash_blog_newline_hack__ NX__ash_blog_newline_hack__ __ash_blog_newline_hack__ . This function can run as regular elixir code, or it can be compiled to run using multiple backends meant for numerical processing, including running on the GPU. The code that is actually compiled under the hood looks__ash_blog_newline_hack__ __ash_blog_newline_hack__ dramatically__ash_blog_newline_hack__ __ash_blog_newline_hack__ different from what you see in front of you. This is not a bad thing! If you had to use or write the actual underlying code, you’d get nothing done. Check out Nx’s documentation for more.__ash_blog_newline_hack__
__ash_blog_newline_hack__defn softmax(t) do__ash_blog_newline_hack__ Nx.exp(t) / Nx.sum(Nx.exp(t))__ash_blog_newline_hack__end
__ash_blog_newline_hack__ Here is an example from__ash_blog_newline_hack__Nebulex
, an ETS based caching library. In this example, the metaprogramming is quite hidden from you. The actual logic happens in a__ash_blog_newline_hack__ __ash_blog_newline_hack__ __ash_blog_newline_hack__ compiler callback__ash_blog_newline_hack__ __ash_blog_newline_hack__ __ash_blog_newline_hack__ . It involves rewriting the functions to intercept calls and decide if the cached version should be used, or if the value should be computed and cached. This kind of macro is quite magical given how indirect it is, but at the same time in applications where cached functions are common, it stops feeling like magic and starts feeling like a language feature. If I was looking at a large code-base that used this once, I’d likely be concerned. The mental overhead of understanding it for one single case likely isn’t worth it.__ash_blog_newline_hack__
defmodule MyApp.Example do__ash_blog_newline_hack__ use Nebulex.Caching__ash_blog_newline_hack____ash_blog_newline_hack__ @decorate cacheable(cache: MyApp.Cache, key: :cache_key)__ash_blog_newline_hack__ def get_by_name(name, age) do__ash_blog_newline_hack__ ...__ash_blog_newline_hack__ end__ash_blog_newline_hack__end
__ash_blog_newline_hack__ This one is from__ash_blog_newline_hack__Ash
. We do similar magic to the__ash_blog_newline_hack__Nx
example from above for this. Specifically, we have an expression that can be run natively to a data layer (i.e__ash_blog_newline_hack__SQL
) or in elixir. And Ash reserves the right to decide where it gets run, depending on the context and requirements.__ash_blog_newline_hack__
calculate :full_name, :string, expr(first_name <> " " <> last_name)
__ash_blog_newline_hack__ As you can see, all of these examples represent a significant amount of complexity boiled down to very simple macros. This will also be true of the upcoming examples of “configuration macros”, but the big difference here is that each macro call represents very complex code being executed on your behalf.__ash_blog_newline_hack__
__ash_blog_newline_hack____ash_blog_newline_hack__ Lets take a look at some examples of macros that I would consider__ash_blog_newline_hack__ __ash_blog_newline_hack__ less__ash_blog_newline_hack__ __ash_blog_newline_hack__ magical.__ash_blog_newline_hack__
__ash_blog_newline_hack____ash_blog_newline_hack__ This one is likely familiar to most people who have worked with Elixir professionally. Here we have an Ecto.Schema. In this example we actually have three separate macros. The__ash_blog_newline_hack__schema/2
macro sets up some module attributes, and__ash_blog_newline_hack__field/2
adds to those module attributes.__ash_blog_newline_hack__timestamps/0
sets up the schema to track__ash_blog_newline_hack__inserted_at
and__ash_blog_newline_hack__updated_at
timestamps. On its own, however, an__ash_blog_newline_hack__Ecto.Schema
doesn’t really__ash_blog_newline_hack__ __ash_blog_newline_hack__ do__ash_blog_newline_hack__ __ash_blog_newline_hack__ anything. There also isn’t a bunch of code hidden behind the scenes that you need to be aware of being executed. This sets up some information on the module that can be read back later. For example:__ash_blog_newline_hack__MyApp.MySchema.__schema__(:fields)
would return__ash_blog_newline_hack__[:field_name, :inserted_at, :updated_at]
in this case. This is a classic example of what I’d call a “configuration macro”.__ash_blog_newline_hack__
defmodule MyApp.MySchema do__ash_blog_newline_hack__ schema "table" do__ash_blog_newline_hack__ field :field_name, :type__ash_blog_newline_hack____ash_blog_newline_hack__ timestamps()__ash_blog_newline_hack__ end__ash_blog_newline_hack__end
__ash_blog_newline_hack__ Here we can see an example from__ash_blog_newline_hack__Absinthe
‘s documentation. Absinthe is a tool for building GraphQL apis with Elixir. This one slightly straddles the line between “configuration macros” and “metaprogramming macros”, but I think still lands on the side of a “configuration macro”. We configure a field in our GraphQL API, and how to resolve it. There are quite a few macros involved in building an API with__ash_blog_newline_hack__Absinthe
. Here we configure what code gets run to resolve a given field, but ultimately the responsibility for what code runs is our own.__ash_blog_newline_hack__
defmodule BlogWeb.Schema do__ash_blog_newline_hack__ use Absinthe.Schema__ash_blog_newline_hack__ ...__ash_blog_newline_hack____ash_blog_newline_hack__ query do__ash_blog_newline_hack__ @desc "Get all posts"__ash_blog_newline_hack__ field :posts, list_of(:post) do__ash_blog_newline_hack__ resolve &Resolvers.Content.list_posts/3__ash_blog_newline_hack__ end__ash_blog_newline_hack__ end__ash_blog_newline_hack__end
__ash_blog_newline_hack__ Finally, lets take a look at some code from an__ash_blog_newline_hack__Ash.Resource
. I’ll choose a relatively complex example, specifically the defining of an “action”.__ash_blog_newline_hack__
create :create do__ash_blog_newline_hack__ primary? true__ash_blog_newline_hack____ash_blog_newline_hack__ accept [:text]__ash_blog_newline_hack____ash_blog_newline_hack__ argument :public, :boolean do__ash_blog_newline_hack__ allow_nil? false__ash_blog_newline_hack__ default true__ash_blog_newline_hack__ end__ash_blog_newline_hack____ash_blog_newline_hack__ change fn changeset, _ ->__ash_blog_newline_hack__ if Ash.Changeset.get_argument(changeset, :public) do__ash_blog_newline_hack__ Ash.Changeset.force_change_attribute(changeset, :visibility, :public)__ash_blog_newline_hack__ else__ash_blog_newline_hack__ changeset__ash_blog_newline_hack__ end__ash_blog_newline_hack__ end__ash_blog_newline_hack____ash_blog_newline_hack__ change relate_actor(:author)__ash_blog_newline_hack__end
__ash_blog_newline_hack__ For those not familiar with Ash, lets break it down.__ash_blog_newline_hack__
__ash_blog_newline_hack____ash_blog_newline_hack__primary? true
means that if something wants to create one of these, this is the action it should call by default. This is used by certain internal actions and can be used by you or other extensions to determine which action to call in lieu of an explicitly configured action.__ash_blog_newline_hack__
__ash_blog_newline_hack__accept [:text]
means that you can set the__ash_blog_newline_hack__text
attribute when calling this action.__ash_blog_newline_hack__
__ash_blog_newline_hack__argument :public, :boolean, …
adds an additional input to the action that doesn’t map to an attribute, called__ash_blog_newline_hack__:public
that must have a value, but defaults to__ash_blog_newline_hack__true
.__ash_blog_newline_hack__
__ash_blog_newline_hack__ The anonymous function you see with__ash_blog_newline_hack__change fn changeset, _ ->
is what we call an “inline change”. A__ash_blog_newline_hack__change
is a function that takes a changes and returns a changeset. If you’ve worked with Phoenix or with Plug, you’ll recognize this pattern, it works effectively the same as__ash_blog_newline_hack__Plug
, working on a__ash_blog_newline_hack__Conn
. Except in this case, its a__ash_blog_newline_hack__change
working on an__ash_blog_newline_hack__Ash.Changeset
. We use this argument to map a boolean toggle input to an enumerated attribute.__ash_blog_newline_hack__change relate_actor(:author)
refers to a “built in change”. This change is provided by Ash out of the box, and this relates the actor (A.K.A current user) to the thing that was just created as the__ash_blog_newline_hack__:author
.__ash_blog_newline_hack__
__ash_blog_newline_hack__ These are all macros! However, they again map to an introspectable structure, acting more as configuration than as a traditional macro.__ash_blog_newline_hack__
__ash_blog_newline_hack__iex(1)> Ash.Resource.Info.action(Twitter.Tweets.Tweet, :create)__ash_blog_newline_hack__%Ash.Resource.Actions.Create{__ash_blog_newline_hack__ name: :create,__ash_blog_newline_hack__ primary?: true,__ash_blog_newline_hack__ type: :create,__ash_blog_newline_hack__ …__ash_blog_newline_hack__ arguments: [__ash_blog_newline_hack__ %Ash.Resource.Actions.Argument{__ash_blog_newline_hack__ name: :public,__ash_blog_newline_hack__ …__ash_blog_newline_hack__ }__ash_blog_newline_hack__ ],__ash_blog_newline_hack__ changes: [__ash_blog_newline_hack__ %Ash.Resource.Change{...},__ash_blog_newline_hack__ %Ash.Resource.Change{__ash_blog_newline_hack__ change: {Ash.Resource.Change.RelateActor,__ash_blog_newline_hack__ [allow_nil?: false, relationship: :author]},__ash_blog_newline_hack__ …__ash_blog_newline_hack__ }__ash_blog_newline_hack__ ],__ash_blog_newline_hack__ …__ash_blog_newline_hack__}
__ash_blog_newline_hack__ This inversion of control is something we’re familiar with in other tools like__ash_blog_newline_hack__Plug
or__ash_blog_newline_hack__Phoenix.Endpoint/Router
. They are great examples of how this pattern can allow for code reuse, and make complex chains of behavior easier to reason about.__ash_blog_newline_hack__
__ash_blog_newline_hack__ Above I’ve made a case for macros not necessarily being what gives Ash its relatively steep learning curve. We’ve also create a large suite of tools to mitigate the difficulties that these configuration macros can have. Take a look at__ash_blog_newline_hack__ __ash_blog_newline_hack__ https://hex.pm/packages/spark__ash_blog_newline_hack__ __ash_blog_newline_hack__ for more information on those tools.__ash_blog_newline_hack__
__ash_blog_newline_hack____ash_blog_newline_hack__ With that said, in that one example above, we had to add five new words to our vocabulary. This is what can make Ash difficult. When you set out to write your own application patterns, you can let your own vocabulary evolve. This provides a natural learning curve. I’d also argue that it gives you tons of opportunities to repeat the mistakes of every developer who went through that same process.__ash_blog_newline_hack__
__ash_blog_newline_hack____ash_blog_newline_hack__ Ash presents a huge suite of integrated tools, a suite that gets bigger and bigger every day and, in order to leverage it, you need to learn a whole new way of doing things. This is a very valid reason not to use Ash. Our biggest proponents will tell you that it’s worth it to put in the effort to learn these tools. As would I. And I don’t mean for the idea of what the framework may become, but for the benefits you can get right now.__ash_blog_newline_hack__
__ash_blog_newline_hack____ash_blog_newline_hack__ With that said, I think a diversity of mindsets is the most important thing that we can foster as a community, and there is a fine line between telling people “Hey, I think we’ve got some good ideas over here, I think you should check them out”, and saying “Hey, this is the right way to do things, stop doing things your way and do them our way”. So while I’m of course a proponent of Ash, my plan is not to__ash_blog_newline_hack__ __ash_blog_newline_hack__ talk anyone into using it__ash_blog_newline_hack__ __ash_blog_newline_hack__ . I have one simple goal, which is to continue to expand this integrated suite of tools, adding new capabilities that are only possible for things built in this way. Ash is a snowball, rolling down a hill, and it has barely even begun to gather snow. By doing this, we move the threshold by which cost of learning the “Ash way” is offset by the benefits. For many users (more every day) that threshold has already been crossed. For others, it may never be crossed, and thats okay 😊. If the tools we build can help even one person find success on their Elixir journey, then I’m a happy camper.__ash_blog_newline_hack__
__ash_blog_newline_hack____ash_blog_newline_hack__ The cost of learning something like Ash is not trivial. It depends on you, your problem space, your experience level, and many other factors as to whether or not that juice is worth the squeeze. My goal, and the core team’s goal, is to continue to provide leverage for those already using Ash. To take the patterns we’ve set down and take them even further, increasing the value of our existing users’ investments. Partially because we count among those users, but also because we believe in what we’re building and its ability to help us and others build bigger and better software.__ash_blog_newline_hack__
' +inserted_at: "2023-05-11 19:39:18.027487Z" +updated_at: "2023-05-11 20:00:29.800042Z" --- + A common criticism of Ash is that it has too many macros. This is an understandable position to take, but I think its important to distinguish between two main kinds of macros. One of them should be used extremely sparingly, and the other I think its okay to use a bit more liberally. We'll call them "configuration macros" and "metaprogramming macros". Keep in mind these names are a bit made up and the boundary between the two is not always cut and dry. When people talk about "magic" in the context of macros, they are mostly talking about "metaprogramming macros". Lets take a look at some examples. ## Metaprogramming Macros -This snippet is from [NX](https://github.com/elixir-nx/nx/tree/main/nx#readme). This function can run as regular elixir code, or it can be compiled to run using multiple backends meant for numerical processing, including running on the GPU. The code that is actually compiled under the hood looks *dramatically* different from what you see in front of you. This is not a bad thing! If you had to use or write the actual underlying code, you'd get nothing done. Check out Nx's documentation for more. +This snippet is from [NX](https://github.com/elixir-nx/nx/tree/main/nx#readme). This function can run as regular elixir code, or it can be compiled to run using multiple backends meant for numerical processing, including running on the GPU. The code that is actually compiled under the hood looks _dramatically_ different from what you see in front of you. This is not a bad thing! If you had to use or write the actual underlying code, you'd get nothing done. Check out Nx's documentation for more. ```elixir defn softmax(t) do @@ -28,7 +29,7 @@ defn softmax(t) do end ``` -Here is an example from `Nebulex`, an ETS based caching library. In this example, the metaprogramming is quite hidden from you. The actual logic happens in a [*compiler callback*](https://hexdocs.pm/elixir/1.12/Module.html#module-compile-callbacks). It involves rewriting the functions to intercept calls and decide if the cached version should be used, or if the value should be computed and cached. This kind of macro is quite magical given how indirect it is, but at the same time in applications where cached functions are common, it stops feeling like magic and starts feeling like a language feature. If I was looking at a large code-base that used this once, I'd likely be concerned. The mental overhead of understanding it for one single case likely isn't worth it. +Here is an example from `Nebulex`, an ETS based caching library. In this example, the metaprogramming is quite hidden from you. The actual logic happens in a [_compiler callback_](https://hexdocs.pm/elixir/1.12/Module.html#module-compile-callbacks). It involves rewriting the functions to intercept calls and decide if the cached version should be used, or if the value should be computed and cached. This kind of macro is quite magical given how indirect it is, but at the same time in applications where cached functions are common, it stops feeling like magic and starts feeling like a language feature. If I was looking at a large code-base that used this once, I'd likely be concerned. The mental overhead of understanding it for one single case likely isn't worth it. ```elixir defmodule MyApp.Example do @@ -51,9 +52,9 @@ As you can see, all of these examples represent a significant amount of complexi ## Configuration Macros -Lets take a look at some examples of macros that I would consider *less* magical. +Lets take a look at some examples of macros that I would consider _less_ magical. -This one is likely familiar to most people who have worked with Elixir professionally. Here we have an Ecto.Schema. In this example we actually have three separate macros. The `schema/2` macro sets up some module attributes, and `field/2` adds to those module attributes. `timestamps/0` sets up the schema to track `inserted_at` and `updated_at` timestamps. On its own, however, an `Ecto.Schema` doesn't really *do* anything. There also isn't a bunch of code hidden behind the scenes that you need to be aware of being executed. This sets up some information on the module that can be read back later. For example: `MyApp.MySchema.__schema__(:fields)` would return `[:field_name, :inserted_at, :updated_at]` in this case. This is a classic example of what I'd call a "configuration macro". +This one is likely familiar to most people who have worked with Elixir professionally. Here we have an Ecto.Schema. In this example we actually have three separate macros. The `schema/2` macro sets up some module attributes, and `field/2` adds to those module attributes. `timestamps/0` sets up the schema to track `inserted_at` and `updated_at` timestamps. On its own, however, an `Ecto.Schema` doesn't really _do_ anything. There also isn't a bunch of code hidden behind the scenes that you need to be aware of being executed. This sets up some information on the module that can be read back later. For example: `MyApp.MySchema.__schema__(:fields)` would return `[:field_name, :inserted_at, :updated_at]` in this case. This is a classic example of what I'd call a "configuration macro". ```elixir defmodule MyApp.MySchema do @@ -115,7 +116,7 @@ For those not familiar with Ash, lets break it down. - `argument :public, :boolean, …` adds an additional input to the action that doesn’t map to an attribute, called `:public` that must have a value, but defaults to `true`. - The anonymous function you see with `change fn changeset, _ ->` is what we call an "inline change". A `change` is a function that takes a changes and returns a changeset. If you’ve worked with Phoenix or with Plug, you’ll recognize this pattern, it works effectively the same as `Plug`, working on a `Conn`. Except in this case, its a `change` working on an `Ash.Changeset`. We use this argument to map a boolean toggle input to an enumerated attribute. -`change relate_actor(:author)` refers to a "built in change". This change is provided by Ash out of the box, and this relates the actor (A.K.A current user) to the thing that was just created as the `:author`. + `change relate_actor(:author)` refers to a "built in change". This change is provided by Ash out of the box, and this relates the actor (A.K.A current user) to the thing that was just created as the `:author`. These are all macros! However, they again map to an introspectable structure, acting more as configuration than as a traditional macro. @@ -156,7 +157,7 @@ With that said, in that one example above, we had to add five new words to our v Ash presents a huge suite of integrated tools, a suite that gets bigger and bigger every day and, in order to leverage it, you need to learn a whole new way of doing things. This is a very valid reason not to use Ash. Our biggest proponents will tell you that it’s worth it to put in the effort to learn these tools. As would I. And I don’t mean for the idea of what the framework may become, but for the benefits you can get right now. -With that said, I think a diversity of mindsets is the most important thing that we can foster as a community, and there is a fine line between telling people "Hey, I think we’ve got some good ideas over here, I think you should check them out", and saying "Hey, this is the right way to do things, stop doing things your way and do them our way". So while I’m of course a proponent of Ash, my plan is not to *talk anyone into using it*. I have one simple goal, which is to continue to expand this integrated suite of tools, adding new capabilities that are only possible for things built in this way. Ash is a snowball, rolling down a hill, and it has barely even begun to gather snow. By doing this, we move the threshold by which cost of learning the "Ash way" is offset by the benefits. For many users (more every day) that threshold has already been crossed. For others, it may never be crossed, and thats okay 😊. If the tools we build can help even one person find success on their Elixir journey, then I’m a happy camper. +With that said, I think a diversity of mindsets is the most important thing that we can foster as a community, and there is a fine line between telling people "Hey, I think we’ve got some good ideas over here, I think you should check them out", and saying "Hey, this is the right way to do things, stop doing things your way and do them our way". So while I’m of course a proponent of Ash, my plan is not to _talk anyone into using it_. I have one simple goal, which is to continue to expand this integrated suite of tools, adding new capabilities that are only possible for things built in this way. Ash is a snowball, rolling down a hill, and it has barely even begun to gather snow. By doing this, we move the threshold by which cost of learning the "Ash way" is offset by the benefits. For many users (more every day) that threshold has already been crossed. For others, it may never be crossed, and thats okay 😊. If the tools we build can help even one person find success on their Elixir journey, then I’m a happy camper. ## Parting Words