Skip to content

feat: add ~z sigil for cartesian and polar complex numbers - #33

Closed
anquegi wants to merge 3 commits into
elixir-nx:mainfrom
anquegi:add_z_sigil
Closed

anquegi wants to merge 3 commits into
elixir-nx:mainfrom
anquegi:add_z_sigil

Conversation

@anquegi

@anquegi anquegi commented Nov 9, 2026

Copy link
Copy Markdown

Add ~z sigil for complex numbers

Adds a ~z sigil to build complex numbers from literals:

  import Complex.Sigils

  ~z(3 4)       #=> Complex.new(3, 4)
  ~z(2 1.57)p   #=> Complex.from_polar(2, 1.57)

The two numbers are separated by whitespace. Without modifiers they are
the real and imaginary parts; with the p modifier they are the magnitude
and the angle (radians).

Motivation

  • Writing complex values (phasors, impedances in circuit analysis) with
    Complex.new/2 is verbose. ~z(3 4) is much more pleasant to write
    and read.
  • The cartesian form mirrors Common Lisp's #c(re im) reader syntax.
    The p modifier (polar) is an extension specific to this library.
  • z is the conventional name for a complex variable (z = x + iy).
  • Being a lowercase sigil, it supports interpolation: ~z(#{re} #{im}).

Changes

  • Complex.Sigils (new): sigil_z/2, one clause per modifier. Integers
    and floats are kept as written. Invalid input raises ArgumentError
    (bad number) or MatchError (not exactly two numbers); an unknown
    modifier raises FunctionClauseError.
  • Complex.Kernel: use Complex.Kernel now also imports sigil_z/2.
    The sigil can be imported on its own with import Complex.Sigils.
  • README: new "Sigil" section.

Tests

New tests in test/complex/sigils_test.exs and
test/complex/kernel_sigil_test.exs (8 tests): cartesian and polar forms,
integer/float preservation, whitespace handling, error cases, and the
use Complex.Kernel import. mix format --check-formatted passes.

Notes

  • The Complex.acot/1 doctest (lib/complex.ex) fails on Elixir 1.20.4 /
    OTP 29 on main as well, due to last-bit float rounding. It is
    unrelated to this PR.

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
@josevalim

Copy link
Copy Markdown
Contributor

If we go down this route, we should avoid single letter sigils due to the chance of conflicts.

Comment thread lib/complex/kernel.ex Resolved
Comment thread lib/complex/sigils.ex Resolved
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
@anquegi

anquegi commented Nov 9, 2026

Copy link
Copy Markdown
Author

Thanks for the reviews! I pushed 0c08ae0, which removes the sigil import
from Complex.Kernel (and its test), so use Complex.Kernel goes back to
only overloading the operators. The sigil is now opt-in via an explicit
import Complex.Sigils, which should also remove the risk of clashes.

On the naming, I'm happy to follow your preference, but here is the
trade-off for the record:

  • ~z is compact, and being lowercase it interpolates: ~z(#{re} #{im}).

  • Multi-letter names must be uppercase (~complex is rejected by the
    compiler on 1.20.4), and uppercase sigils don't interpolate, so
    ~COMPLEX/~POLAR would only cover literals:

    iex> x = 3
    iex> ~COMPLEX("#{x} 4")
    "\"\#{x} 4\""
    

    For variables, Complex.new/2 remains the way to go.

If you still prefer to avoid single-letter sigils, I'll switch to
~COMPLEX and ~POLAR. Would you also be open to Complex.Kernel
importing those two, or should they stay behind the explicit
import Complex.Sigils?

@polvalente

Copy link
Copy Markdown
Member

compiler on 1.20.4), and uppercase sigils don't interpolate, so
~COMPLEX/~POLAR would only cover literals:

This is true, but I think if interpolation really is needed, one can either use ~COMPLEX("...") with the inner quotes handling the interpolation into a string; or use the Complex.new or Complex.from_polar functions.

I believe the sigil is strongest for literals anyway, so discouraging interpolation is a feature IMO

If you still prefer to avoid single-letter sigils, I'll switch to ~COMPLEX and ~POLAR. Would you also be open to Complex.Kernel importing those two, or should they stay behind the explicit import Complex.Sigils?

As I mentioned, the intent is for use Complex.Kernel to provide only an Elixir.Kernel override, so we shouldn't do it.
On this note, we could also add them to Complex and direct users to import Complex, only: :sigils to avoid creating a module just for this.

@josevalim

Copy link
Copy Markdown
Contributor

I'd say interpolation is a good reason to use Complex.new or Complex.polar, it will definitely be much clearer.

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
@anquegi

anquegi commented Nov 9, 2026 •

Copy link
Copy Markdown
Author

Thanks both, that makes sense. I pushed ceed5d4:

  • ~COMPLEX and ~POLAR now live in Complex, so the separate
    Complex.Sigils module is gone. Use import Complex, only: :sigils.
  • Complex.Kernel is untouched (it only overrides the operators).
  • No modifiers anymore; interpolation is left to Complex.new/2 and
    Complex.from_polar/2, as suggested.
  • Tests and README updated.

I'll also update the PR description to match. But, just in case this has sense anymore because probably it has no sense to write ~Complex instead of Complex.new, maybe I focused more in write less than clarity of code. I'm not sure that this really has an utility it was for the sake of defining a capacitator:

omega = 2.0 * 3.141592653589793 * f_hz

# Forma Polar: Zc = Magnitud ∠ Ángulo(-90° en radianes)
magnitud = 1.0 / (omega * c_faradios)
angulo_rad = -3.141592653589793 / 2.0

z_c = Complex.from_polar(magnitud, angulo_rad)

omega = 2.0 * 3.141592653589793 * f_hz

# Forma Rectangular: Zc = Real + j*Imaginario
z_c = Complex.new(0.0, -1.0 / (omega * c_faradios))

But really the I need to write all to use ~z(magnitud, angulo_rad)p

I think that I'm wasting your time (I'm so sorry for that), and maybe I should abstract all this to a function that calculates this, I was blind by the Money library and the use of sigil on it. and maybe the problem is not to write the complex number quickly is to calculate it. Thanks to explain this to me. and I think that I made all the changes that you require, so is up to you, and finally I'm glad to spoke with two real real real great developers.

@anquegi

anquegi commented Nov 11, 2026

Copy link
Copy Markdown
Author

Thanks both for the thorough reviews and for the patience.

After going through the feedback, I agree this doesn't pull its weight: once interpolation and modifiers are out, ~COMPLEX("3 4") only saves a few keystrokes over Complex.new(3, 4), and it adds API surface to maintain. The real problem I was trying to solve (building impedances) is about computing values, not writing literals, so a domain-specific function is the better fit.

I'm closing this PR. Appreciate the time you both put into it.

@anquegi anquegi closed this Nov 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants