Proposal: [DDD] A new operator for making tagged tuples to express domain business rules for data of primitive types.

140 views
Skip to first unread message

Ivan Rublev

unread,
Apr 11, 2020, 2:56:59 PM4/11/20
to elixir-lang-core
Problem

One way of representing domain model business rules (constraints) is to do pattern matching in function definitions against the correct shape of data, making a function calls with data formed incorrectly to fail. And appropriate @specs helps Dialyzer find many failing calls before runtime.

That works well for domain entities representable with a struct (or a record). Each piece of an entity's data is tagged with a struct's key. Then a business rule can be expressed with pattern-matching against the appropriate key. E.g. for entity %Order{id: int, valid: bool} we can implement the business rule "send only valid orders" with def send(%Order{valid: true} = order)... function definition.

There are business rules related to data of primitive types that should be fulfilled throughout the application and in same time hardly expressable with pattern-matching or common type specs. For example "Order should have an id that is related only to the Order and no any other entitiy." or "Speed should be calculated only in m/s, and we can have meters or kilometers as Path length."
    @spec process(Order.t()) :: Order.t()
   
def process(%Order{id: integer} = order)...

   
@spec speed(path :: float, duration :: float) :: float  <---- How can we express business rules mentioned above?
   
def speed(path, time)...                    


Proposal

One possible valid way to do so is to wrap the data of primitive type in a tagged tuple, as suggested in the Sequential Programming chapter of Getting Started with Erlang.
That may seem unelegant to reintroduce lots of curled brackets, can look like opening gates for noisy code, and may generally seem against the lightweight structure of Elixir. 

It's not that exactly. Here is how the business rule for Order id can look like:
    defmodule Order do
        defstruct
[:id, ...]
        deftag
Id, for_value_of: non_neg_integer
       
@type t :: %__MODULE__{id: Id.t(), ...}
   
end

    alias Order.Id, as: OrderId

   
def process(%Order{id: OrderId ~ id} == order)...


And the rule for speed data can look like the following:
    deftag Meter, for_value_of: float
    deftag
Kilometer, for_value_of: float
    deftag
Path, for_value_of: Meter.t() | Kilometer.t()
    deftag
Sec, for_value_of: float
    deftag
MeterPerSec, for_value_of: float

   
@spec speed(path :: Path.t(), duration :: Sec.t()) :: MeterPerSec.t()
   
def speed(Path ~ Meter ~ path, Sec ~ duration), do: MeterPerSec ~ path / duration
   
def speed(Path ~ Kilometer ~ path, Sec ~ duration), do: MeterPerSec ~ path * 1000.0 / duration

    speed
(Path ~ Kilometer ~ 30.0, Sec ~ 3600 * 3600)


Where above, it'd be great to have ~ as an operator with the right to left associativity, making two-element tuple as a ~ b ~ c == {a, {b, c}}.

Safety checks

There are different examples of possible ~ operator application emulated with defmacro >>> in the code snippet below. See the definition of deftag there too.

Dialyzer catches all primitive type mismatches in function calls and missing cases of case/2 macro.
    mix dialyzer --format short

    lib
/variants.ex:38:call The function call speed will not succeed.
   
...
    lib
/variants.ex:89:call The function call print_score will not succeed.


Problems and workarounds

It's still possible to make a structure with untagged values with map syntax like %Order{id: "foo"}. Dialyzer catches the mismatch in a call to a function which unpacks id value from the tagged tuple. To make the failure closer to the place of the error, one can define and use a new/1 function in combination with @opaque t for making a struct.

It's still possible to make a tuple with a nonexistent module name like {Nonexisting, 1}. It'd be great to have a flag that makes the compiler to check if the name of the module used is defined anywhere at the end of the compilation.

Wrap up

Making Domain Models DSL and defining always valid constraints for entities data of primitive types is a common practice in Domain Driven Design.

Enforcing constraints to be valid on every step of application execution by means of the type system is inspired by the Scott Wlaschins book - Domain Modeling Made Functional. Tackle Software Complexity with Domain-Driven Design and F#.


A new ~ operator for making a two-element tuple can dramatically lightweight the definition and pattern matching for nested tagged tuples.

Is it an absurd idea to introduce that operator?



Example applications

defmodule Tag do
  defmacro deftag
(name, for_value_of: type) do
    quote
do
      defmodule unquote
(name), do: @type(t :: {__MODULE__, unquote(type)})
   
end
 
end

  defmacro deftag
(name) do
    quote
do
      defmodule unquote
(name), do: @type(t :: __MODULE__)
   
end
 
end

  defmacro a
>>> b do
    quote
do
     
{unquote(a), unquote(b)}
   
end
 
end
end

defmodule
Variants do
  defmodule
Speed do
   
import Tag

    deftag
Meter, for_value_of: float
    deftag
Kilometer, for_value_of: float
    deftag
Sec, for_value_of: float
    deftag
MeterPerSec, for_value_of: float

   
@type path :: Meter.t() | Kilometer.t()
   
@type duration :: Sec.t()
   
@type speed :: MeterPerSec.t()

   
@spec speed(path, duration) :: speed
   
def speed(Meter >>> path, Sec >>> duration), do: MeterPerSec >>> (path / duration)
   
def speed(Kilometer >>> path, Sec >>> duration), do: MeterPerSec >>> (path * 1000.0 / duration)

   
def speed_fail_1(), do: speed(1.0, 3.0)

   
def speed_fail_2(), do: speed(Sec >>> 1.0, Meter >>> 2.0)

   
@spec speed_ok :: MeterPerSec.t()
   
def speed_ok(), do: speed(Kilometer >>> 0.01, Sec >>> 2.0)
 
end

  defmodule
Tennis do
    # https://en.wikipedia.org/wiki/Tennis_scoring_system
   
import Tag

    deftag
Player, for_value_of: :one | :two
    deftag
PointValue, for_value_of: :love | :fifteen | :thirty

    deftag
Points, for_value_of: {PointValue.t(), PointValue.t()}
    deftag
Forty, for_value_of: {Player.t(), PointValue.t()}   
    deftag
Deuce # option has no associated value
    deftag
Advantage, for_value_of: Player.t()
    deftag
Game, for_value_of: Player.t()

    deftag
Score, for_value_of: Points.t() | Forty.t() | Deuce.t() | Advantage.t() | Game.t()

   
@spec print_score(Score.t()) :: :ok
   
def print_score(Score >>> score) do
      IO
.puts(
       
case score do
         
Points >>> {PointValue >>> p1, PointValue >>> p2} ->
           
"points: #{Atom.to_string(p1)}, #{Atom.to_string(p2)}"

         
Deuce ->
           
"both players have 40 points"

         
Advantage >>> (Player >>> player) ->
           
"player #{Atom.to_string(player)} has 41 and other 40"
       
end
     
)
   
end

   
@spec score_ok(:start | :middle | :final) :: :ok
   
def score_ok(:start),
     
# add brackets to overcome left to right precedence
     
do: print_score(Score >>> (Points >>> {PointValue >>> :love, PointValue >>> :love}))

   
def score_ok(:middle),
     
do: print_score(Score >>> Deuce)

   
def score_ok(:finals),
     
do: print_score(Score >>> (Advantage >>> (Player >>> :one)))

   
def score_fail_print(),
     
do: print_score(Score >>> (Game >>> :one))
 
end

  defmodule CRM
do
   
import Tag

    defmodule
Order do
     
@keys [
       
:id,
       
:customer_id,
       
:shipping_address,
       
:billing_address,
       
:order_lines,
       
:amount_to_bill
     
]
     
@enforce_keys @keys
      defstruct
@keys

      deftag
Id, for_value_of: String.t()
      deftag
CustomerId, for_value_of: String.t()
      deftag
BillingAmount # type of the associated value hasn't been known yet.

     
@type t :: %__MODULE__{
              id
: Id.t(),
              customer_id
: CustomerId.t(),
              shipping_address
: Address.t(),
              billing_address
: Address.t(),
              order_lines
: [OrderLine.t()],
              amount_to_bill
: BillingAmount.t()
           
}

     
@spec new(
             
Id.t(),
             
CustomerId.t(),
              shipping_address
:: Address.t(),
              billing_address
:: Address.t(),
             
[OrderLine.t()],
             
BillingAmount.t()
           
) :: t()
     
def new(id, customer_id, shipping_address, billing_address, order_lines, amount_to_bill) do
       
%__MODULE__{
          id
: id,
          customer_id
: customer_id,
          shipping_address
: billing_address,
          billing_address
: shipping_address,
          order_lines
: order_lines,
          amount_to_bill
: amount_to_bill
       
}
     
end
   
end

    defmodule
Order.Address do
     
@enforce_keys [:country, :city, :street1, :house]
      defstruct
[:country, :city, :street1, :street2, :house, :comment]

      deftag
Country, for_value_of: :usa | :germany | :brazil
      deftag
City, for_value_of: String.t()
      deftag
Street, for_value_of: String.t()
      deftag
House, for_value_of: String.t()
      deftag
Comment, for_value_of: String.t()

     
@type t :: %__MODULE__{
              country
: Country.t(),
              city
: City.t(),
              street1
: Street.t(),
              street2
: Street.t() | nil,
              house
: House.t(),
              comment
: Comment.t() | nil
           
}

     
@spec new(Country.t(), City.t(), Street.t(), House.t()) :: t()
     
def new(country, city, street1, house) do
       
%__MODULE__{country: country, city: city, street1: street1, house: house}
     
end
   
end

    defmodule
Order.OrderLine do
     
@keys [:id, :order_id, :product_code, :order_quantity, :price]
     
@enforce_keys @keys
      defstruct
@keys

      deftag
Id, for_value_of: non_neg_integer
      deftag
ProductCode, for_value_of: String.t()

      deftag
Unit, for_value_of: non_neg_integer
      deftag
Kilos, for_value_of: float
      deftag
OrderQuantity, for_value_of: Unit.t() | Kilos.t()

      deftag
Price, for_value_of: float

     
@opaque t :: %__MODULE__{
                id
: Id.t(),
                order_id
: Order.Id.t(),
                product_code
: ProductCode.t(),
                order_quantity
: OrderQuantity.t(),
                price
: Price.t()
             
}

     
@spec new(Id.t(), Order.Id.t(), ProductCode.t(), OrderQuantity.t(), Price.t()) :: t()
     
def new(id, order_id, product_code, order_quantity, price) do
       
%__MODULE__{
          id
: id,
          order_id
: order_id,
          product_code
: product_code,
          order_quantity
: order_quantity,
          price
: price
       
}
     
end
   
end

   
# usage

   
alias Order.{Id, CustomerId, Address, OrderLine, BillingAmount}
   
alias Order.Address.{Country, City, Street, House}
   
alias Order.OrderLine.Id, as: OrderLineId
   
alias Order.OrderLine.{ProductCode, Kilos, Unit, OrderQuantity, Price}

   
@spec new_order_bananas(
            id
:: String.t(),
            customer_id
:: String.t(),
            country
:: :usa | :germany,
            city
:: String.t(),
            street
:: String.t(),
            house
:: String.t(),
            kilos
:: float,
            price
:: float
         
) :: Order.t()
   
def new_order_bananas(
          id
,
          customer_id
,
          country
,
          city
,
          street
,
          house
,
          kilos
,
          price
       
) do
      addr
= Address.new(Country >>> country, City >>> city, Street >>> street, House >>> house)

     
Order.new(
       
Id >>> id,
       
CustomerId >>> customer_id,
        addr
,
        addr
,
       
[
         
OrderLine.new(
           
OrderLineId >>> 0,
           
Id >>> id,
           
ProductCode >>> "AHBN",
           
OrderQuantity >>> (Kilos >>> kilos),
           
Price >>> price
         
)
       
],
       
BillingAmount
     
)
   
end

   
@spec add_line(
            order
:: Order.t(),
            code
:: ProductCode.t(),
            units
:: Unit.t(),
            price
:: Price.t()
         
) :: Order.t()
   
def add_line(%Order{id: ord_id, order_lines: lines} = order, code, units, price) do
     
%OrderLine{id: OrderLineId >>> ln_id} =
       
Enum.max_by(lines, fn %OrderLine{id: OrderLineId >>> id1} -> id1 end)

     
%Order{
        order
       
| order_lines: [
           
OrderLine.new(
             
OrderLineId >>> (ln_id + 1),
              ord_id
,
              code
,
             
OrderQuantity >>> units,
              price
           
)
           
| lines
         
]
     
}
   
end

   
@spec bananas_order_ok :: Order.t()
   
def bananas_order_ok do
      order
=
        new_order_bananas
(
         
"12143",
         
"AXC-0033",
         
:germany,
         
"Hamburg",
         
"Feldstrasse",
         
"18",
         
120.0,
         
0.35
       
)

      add_line
(order, ProductCode >>> "BOX-005", Unit >>> 1, Price >>> 1.00)
   
end

   
def oranges_line_dialyzer_fail do
     
OrderLine.new(
       
1125,
       
Id >>> nil,
       
ProductCode >>> "AHORJ",
       
OrderQuantity >>> (Kilos >>> 10),
       
Price >>> 0.14
     
)
   
end
 
end
end


José Valim

unread,
Apr 11, 2020, 3:09:53 PM4/11/20
to elixir-l...@googlegroups.com
Thank you for the detailed proposal.

Unfortunately adding ~ as an operator will introduce ambiguities since it is already used for sigils. For example, if introduced, foo~r”foo” can be read either as foo ~ r(“foo”) or as foo(~r”foo”).

Ultimately, we don’t plan to introduce new operators beyond the custom operators already available in the language.

Ivan Rublev

unread,
Apr 13, 2020, 6:38:36 AM4/13/20
to elixir-lang-core
Thanks for your message. Turns out that the tilde symbol is confusing as a stand-alone operator.
I can get that current operator list is unexpandable. It's generally hard to find an unused symbol that will be easy to type in with various keyboards.)))

What can be good for fast tuples definition is an operator with right to left associativity and as short as possible to make code lighter.
Turns out the only candidate is | . It's reserved to be defined as custom operator and is right to left. So I expected that I could override it for the purpose.

Unfortunately, when I use it for tuples generation the compiler fails to process it in a function argument:

elixir -v  
Erlang/OTP 22 [erts-10.6.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [hipe]

Elixir 1.10.2 (compiled with Erlang/OTP 21)


defmodule
Bug do

  defmacro a
| b do
    quote
do
     
{unquote(a), unquote(b)}
   
end
 
end


 
def do_foo, do: foo(:first | :next | 1)

 
def foo(:first | :next | bar), do: IO.puts(bar)
end


mix compile
Compiling 1 file (.ex)

== Compilation error in file lib/bug.ex ==
** (FunctionClauseError) no function clause matching in :v3_core.pattern/2    
   
   
The following arguments were given to :v3_core.pattern/2:
   
       
# 1
       
{:call, 10, {:atom, 10, :|}, [{:atom, 0, :first}, {:tuple, 0, [{:atom, 0, :next}, {:var, 10, :_bar@1}]}]}
   
       
# 2
       
{:core, 1, 0, {:foo, 1}, false, true, [extra_chunks: [{"Docs", <<131, 80, 0, 0, 0, 222, 120, 156, 109, 204, 77, 14, 194, 32, 16, 5, 224, 161, 72, 75, 76, 188, 67, 151, 186, 50, 158, 168, 25, 25, 106, 169, 133, 73, 16, 181, 139, 30, 94, ...>>}, {"ExCk", <<131, 104, 2, 100, 0, 17, 101, 108, 105, 120, 105, 114, 95, 99, 104, 101, 99, 107, 101, 114, 95, 118, 49, 116, 0, 0, 0, 2, 100, 0, 7, 101, 120, 112, 111, 114, 116, 115, ...>>}], debug_info: {:elixir_erl, {:elixir_v1, %{attributes: [], compile_opts: [], definitions: [{{:|, 2}, :defmacro, [line: 2], [{[line: 2], [{:a, [version: 0, line: 2], nil}, {:b, [version: 1, line: 2], nil}], [], {{:a, [version: 0, line: 4], nil}, {:b, [version: 1, line: 4], nil}}}]}, {{:foo, 1}, :def, [line: 10], [{[line: 10], [{:|, [line: 10], [:first, {:next, {:bar, [version: 0, line: 10], nil}}]}], [], {{:., [line: 10], [IO, :puts]}, [line: 10], [{:bar, [version: 0, line: 10], nil}]}}]}, {{:do_foo, 0}, :def, [line: 8], [{[line: 8], [], [], {:foo, [line: 8], [first: {:next, 1}]}}]}], deprecated: [], file: "/Users/levvibraun/Repos/Alchemistry/ddd_invariants/lib/bug.ex", is_behaviour: false, line: 1, module: Bug, relative_file: "lib/bug.ex", unreachable: []}, []}}], [], [file: 'lib/bug.ex']}
   
   
(compiler 7.5.1) v3_core.erl:1841: :v3_core.pattern/2
   
(compiler 7.5.1) v3_core.erl:2002: :v3_core.pattern_list/2
   
(compiler 7.5.1) v3_core.erl:272: :v3_core.clause/2
   
(compiler 7.5.1) v3_core.erl:263: :v3_core.clauses/2
   
(compiler 7.5.1) v3_core.erl:233: :v3_core.body/4
   
(compiler 7.5.1) v3_core.erl:221: :v3_core.function/4
   
(compiler 7.5.1) v3_core.erl:182: :v3_core.form/3
   
(stdlib 3.11.1) lists.erl:1263: :lists.foldl/3


Looks like a bug, how is possible to address it?

How do you see the idea of the shortcut for 2 element tuple aligns with the spirit of the language?




On Saturday, April 11, 2020 at 9:09:53 PM UTC+2, José Valim wrote:
Thank you for the detailed proposal.

Unfortunately adding ~ as an operator will introduce ambiguities since it is already used for sigils. For example, if introduced, foo~r”foo” can be read either as foo ~ r(“foo”) or as foo(~r”foo”).

Ultimately, we don’t plan to introduce new operators beyond the custom operators already available in the language.

José Valim

unread,
Apr 13, 2020, 6:49:05 AM4/13/20
to elixir-l...@googlegroups.com
If you introduce this operator, you make the following completely valid syntax today ambiguous:

    [:foo | :bar]

It should not be able to redefine the | operator. We will tag it as a special form and it will raise in future Elixir versions.

I can say upfront that if you pick any operator beyond the list of available operators, you will most likely introduce compiler errors or bugs in the software.

--
You received this message because you are subscribed to the Google Groups "elixir-lang-core" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elixir-lang-co...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/elixir-lang-core/43d98d89-02cb-4d70-a381-639edef02914%40googlegroups.com.

Wiebe-Marten Wijnja

unread,
Apr 13, 2020, 11:32:11 AM4/13/20
to elixir-l...@googlegroups.com

To expand a little on this, there are two other approaches you could take to introduce new syntax in Elixir right now:

1. Define a macro. The arguments you pass to the macro still have to follow Elixir's parsing and precedence rules, but you are able to interpret the AST passed to the macro any way you wish. This is used by a couple of libraries to create very user-friendly DSL's. (An example: Ecto.)

2. Define your own sigil. Inside the sigil's body you can use any syntax that you desire. You can parse the sigil (potentially) at compile-time to provide nice safety checks and other feedback to the user that way. (An example: EEx templates.)

As an aside, your proposal seems to be somewhat in line with what the `Norm' library (https://github.com/keathley/norm) is attempting to do. Maybe an opportunity for cross-pollination of ideas? :-)


Happy Easter,

~Wiebe-Marten / Qqwy

signature.asc

Ivan Rublev

unread,
Apr 14, 2020, 9:31:41 AM4/14/20
to elixir-lang-core
Thanks for the expansion.

It's almost possible to use the macro, please find an example of operator defined with the macro in the first message, like :a >>> (:b >>> 1) == {:a, {:b, 1}}. Unfortunately, we have no any unused overridable operator with right to left associativity available in the language. If we had one then it was easy to define nested tuples in shortest way possible, like a: >>> b: >>> c: >>> d: >>> 1 == {a:, {b:, {c:, {d:, 1}}}}

Thanks for pointing to keathley/norm from my perspective, it's suitable for initial data validation. It supposes to protect functions by wrapping them into contract checking ones. The current proposal is orthogonal to the norm's idea, which means that it'd be great to keep the once verified pieces of data to be explicitly identified throughout the app with tagged tuples and pattern matching. To allow only valid ways of representing primitive data in any involved function definition, to crash explicitly on function call with an incorrectly identified piece of primitive data.



On Monday, April 13, 2020 at 5:32:11 PM UTC+2, Wiebe-Marten Wijnja wrote:

To expand a little on this, there are two other approaches you could take to introduce new syntax in Elixir right now:

1. Define a macro. The arguments you pass to the macro still have to follow Elixir's parsing and precedence rules, but you are able to interpret the AST passed to the macro any way you wish. This is used by a couple of libraries to create very user-friendly DSL's. (An example: Ecto.)

2. Define your own sigil. Inside the sigil's body you can use any syntax that you desire. You can parse the sigil (potentially) at compile-time to provide nice safety checks and other feedback to the user that way. (An example: EEx templates.)

As an aside, your proposal seems to be somewhat in line with what the `Norm' library (https://github.com/keathley/norm) is attempting to do. Maybe an opportunity for cross-pollination of ideas? :-)


Happy Easter,

~Wiebe-Marten / Qqwy


On 13-04-2020 12:48, José Valim wrote:
If you introduce this operator, you make the following completely valid syntax today ambiguous:

    [:foo | :bar]

It should not be able to redefine the | operator. We will tag it as a special form and it will raise in future Elixir versions.

I can say upfront that if you pick any operator beyond the list of available operators, you will most likely introduce compiler errors or bugs in the software.

To unsubscribe from this group and stop receiving emails from it, send an email to elixir-l...@googlegroups.com.
--
You received this message because you are subscribed to the Google Groups "elixir-lang-core" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elixir-l...@googlegroups.com.

Ivan Rublev

unread,
Apr 14, 2020, 9:34:16 AM4/14/20
to elixir-lang-core
Improper lists, indeed. I'm glad that I helped to clear out the issue with |.
Turns out that using any available operator complects things.

What about a simpler new operator approach? I looked at keyboard layouts for MacOS/Linux/Windows, and there are following symbols that are easy to enter with Alt + key on these systems:  « µ ¶ ¿

Is it a ridiculous idea to add one of those symbols as a new operator with the right to left associativity?

For usage like:
:foo«bar "m" == :foo « bar("m") == {:foo, bar("m")}
[:foo « :bar] == [foo: :bar] == [{:foo, :bar}]
[:foo « :bar « :buzz] == [{:foo, {:bar, :buzz}}]



On Monday, April 13, 2020 at 12:49:05 PM UTC+2, José Valim wrote:
If you introduce this operator, you make the following completely valid syntax today ambiguous:

    [:foo | :bar]

It should not be able to redefine the | operator. We will tag it as a special form and it will raise in future Elixir versions.

I can say upfront that if you pick any operator beyond the list of available operators, you will most likely introduce compiler errors or bugs in the software.

To unsubscribe from this group and stop receiving emails from it, send an email to elixir-l...@googlegroups.com.

Justin Wood

unread,
Apr 14, 2020, 3:06:29 PM4/14/20
to elixir-lang-core
> Is it a ridiculous idea to add one of those symbols as a new operator
> with the right to left associativity?

As an Emacs (and light Vim) user, I would personally suggest against
something like this. At least in Emacs, it is not as simple as just
alt+keys in order to get these characters. And even if it were,
newcomers to the language, and people who rarely use these operators,
will have to lookup the key combination, or figure out how to do such a
thing in their editor.

I would suggest that if any new operators were added to the language,
they should be easily typed in as many keyboard layouts as possible.

Justin

José Valim

unread,
Apr 14, 2020, 3:14:56 PM4/14/20
to elixir-l...@googlegroups.com
We could probably add +++ and --- with right associativity to the list, to tag along ++ and -- which are right associative.

--
You received this message because you are subscribed to the Google Groups "elixir-lang-core" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elixir-lang-co...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/elixir-lang-core/C215W5L92I8K.3CLRLHPIZJ885%40nixos.

Ivan Rublev

unread,
Apr 15, 2020, 8:13:01 AM4/15/20
to elixir-lang-core
Thanks for your comment. It may seem frustrating to use a shortcut to enter a symbol, especially when it's not engraved on a keyboard. Same time, we use shortcuts every day to speed up input.
Emacs and Vim are enhanced editors. And I'm glad to hear that you use them. Emacs is very tweakable, and this is one of the brilliant properties of this tool that you can adjust for your comfort. F.e. adding (global-set-key [?\M-\q] [?\x00AB]) to the Emacs Init File gives you « symbol entered when you press Alt+q. And a key combination suitable for you can be assigned. Vim supports special symbols entered as-is coming from the OS input subsystem. F.e. in MacOS Vim, you can press the Alt+\ and have « symbol entered when in Input mode.

A new operator should be easily typed in as many keyboard layouts as possible. Primarily because of that, these symbols « µ ¶ ¿ were suggested.
Please, see input support for these symbols with US-international / UK-extended / German layouts in different OSes:
Windows (Alt + key): https://sites.psu.edu/symbolcodes/windows/codeint/#foreign
Linux (Compose key shortcuts): https://fsymbols.com/keyboard/linux/compose/
macOS (Alt + key): https://fsymbols.com/keyboard/mac/

Ivan Rublev

unread,
Apr 15, 2020, 8:17:39 AM4/15/20
to elixir-lang-core
Adding +++ and --- with right associativity to the list is valuable and generous.

Same time using three symbols for coupling operands can lead to long lines that are harder to read, comparing with one coupling symbol. Can compare the following two equivalent lines where second has 10 characters less:
def speed(Path +++ Kilometer +++ path, Time +++ Sec +++ duration), do: KilometerPerSec +++ path / duration

def speed(Path « Kilometer « path, Time « Sec « duration), do: KilometerPerSec « path / duration


Of course, adding and documenting a special symbol as an operator can lead to a storm of questions about how to type them in, which are not related to the language itself, but more for input tools. What at the end may involve language maintainers/supporters to deal with these questions.

In this context, is it a bad idea alongside with the +++ and --- right associativity operators to allow two following ranges of special symbols for being overridden as operators by experienced engineers: \u00A7-\u00AB (§ ¨ © ª «) with right and \u00BB-\u00BF (» ¼ ½ ¾ ¿) with left associativity?



On Tuesday, April 14, 2020 at 9:14:56 PM UTC+2, José Valim wrote:
We could probably add +++ and --- with right associativity to the list, to tag along ++ and -- which are right associative.

On Tue, Apr 14, 2020 at 9:06 PM 'Justin Wood' via elixir-lang-core <elixir-l...@googlegroups.com> wrote:
> Is it a ridiculous idea to add one of those symbols as a new operator
> with the right to left associativity?

As an Emacs (and light Vim) user, I would personally suggest against
something like this. At least in Emacs, it is not as simple as just
alt+keys in order to get these characters. And even if it were,
newcomers to the language, and people who rarely use these operators,
will have to lookup the key combination, or figure out how to do such a
thing in their editor.

I would suggest that if any new operators were added to the language,
they should be easily typed in as many keyboard layouts as possible.

Justin

--
You received this message because you are subscribed to the Google Groups "elixir-lang-core" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elixir-l...@googlegroups.com.

Louis Pilfold

unread,
Apr 15, 2020, 8:19:51 AM4/15/20
to elixir-lang-core
Hiya

While I'm a big fan of editor customising I think it's important to make technology as accessible as possible. Maybe people (most?) will not know how to either type these characters with their keyboard or know how to configure their editor to enter them on their behalf. Adoption will be slow and the language will seem unfriendly if its features are not usable by all.

Cheers,
Louis


--
You received this message because you are subscribed to the Google Groups "elixir-lang-core" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elixir-lang-co...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/elixir-lang-core/f8ebb77a-540d-44d1-bbee-8efbb101d981%40googlegroups.com.

José Valim

unread,
Apr 15, 2020, 8:23:59 AM4/15/20
to elixir-l...@googlegroups.com
We don't plan to introduce any non-ascii operator to the language.

Ivan Rublev

unread,
Apr 16, 2020, 6:32:05 AM4/16/20
to elixir-lang-core
Ok. I'm good with that.

It'd be great to work on dismissing the possibility of | redefinition and adding support for right-associative +++/2 and ---/2 to be able to be redefined as custom operators. I have time to work on this.

How can we proceed?
To unsubscribe from this group and stop receiving emails from it, send an email to elixir-l...@googlegroups.com.

--
You received this message because you are subscribed to the Google Groups "elixir-lang-core" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elixir-l...@googlegroups.com.

Ivan Rublev

unread,
Apr 18, 2020, 4:43:08 AM4/18/20
to elixir-lang-core
Dear Elixir Core members,

Are any objections against adding +++ and --- with right associativity to the list of custom operators?

José Valim

unread,
Apr 18, 2020, 5:21:56 AM4/18/20
to elixir-l...@googlegroups.com
Please do send a PR.

To unsubscribe from this group and stop receiving emails from it, send an email to elixir-lang-co...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/elixir-lang-core/080c8c18-6d5a-4fa4-934c-26a3d5f06e92%40googlegroups.com.

Ivan Rublev

unread,
Jun 11, 2020, 5:49:11 PM6/11/20
to elixir-lang-core
Hey folks, I made a library based on this proposal for domain modelling with composable tags and runtime type-safe structs. The latter outcome surprised me a lot :)

You can have a look at it here:


https://elixirforum.com/t/user-defined-tags-and-type-safe-structs-for-domain-modelling-domo-library/




On Saturday, April 18, 2020 at 11:21:56 AM UTC+2, José Valim wrote:
Please do send a PR.

Reply all
Reply to author
Forward
0 new messages