Endless methods for Crystal

In looking at doing this raytracer code better/faster, I started rewriting it to conform with my personal style of writing code.

Here is a sample of orignal code that I rewrote to simplify it (to me).

class Sphere < Thing
  @radius2 : Float64

  def initialize(@center : Vector, radius : Float64, @_surface : Surface)
    @radius2 = radius*radius
  end

  def normal(pos)
    Vector.norm(Vector.minus(pos, @center))
  end

  def surface
    @_surface
  end

  def intersect(ray)
    eo = Vector.minus(@center, ray.start)
    v = Vector.dot(eo, ray.dir)
    dist = 0.0
    if (v >= 0)
      disc = @radius2 - (Vector.dot(eo, eo) - v * v)
      if (disc >= 0)
        dist = v - Math.sqrt(disc)
      end
    end
    if (dist == 0)
      return nil
    end
    Intersection.new(self, ray, dist)
  end
end

class Plane < Thing
  def initialize(@_norm : Vector, @offset : Float64, @_surface : Surface)
  end

  def normal(pos)
    return @_norm
  end

  def intersect(ray)
    denom = Vector.dot(@_norm, ray.dir)
    return nil if (denom > 0)
    dist = (Vector.dot(@_norm, ray.start) + @offset) / (-denom)
    return Intersection.new(self, ray, dist)
  end

  def surface
    @_surface
  end
end

abstract class Surface
end

class ShinySurface < Surface
  def diffuse(pos)
    return Color_white
  end

  def specular(pos)
    return Color_grey
  end

  def reflect(pos)
    return 0.7
  end

  def roughness
    return 250
  end
end

I like to read/write code horizontally (as oneliners for single concepts) when possible. It creates code noise (to me) to write a simple oneline concept as a 3 line definition. So here’s how I rewrote the above code to make it cleaner and simpler.

class Sphere < Thing
  @radius2 : Float64

  def initialize(@center : Vector, radius : Float64, @_surface : Surface)
    @radius2 = radius*radius
  end

  def normal(pos); Vector.norm(Vector.minus(pos, @center)) end

  def surface; @_surface end

  def intersect(ray)
    eo = Vector.minus(@center, ray.start)
    v  = Vector.dot(eo, ray.dir)
    dist = 0.0
    if (v >= 0)
      disc = @radius2 - (Vector.dot(eo, eo) - v * v)
      dist = v - Math.sqrt(disc) if (disc >= 0)    
    end
    
    return nil if (dist == 0)

    Intersection.new(self, ray, dist)
  end
end

class Plane < Thing
  def initialize(@_norm : Vector, @offset : Float64, @_surface : Surface) end

  def normal(pos); @_norm end

  def surface;  @_surface end

  def intersect(ray)
    denom = Vector.dot(@_norm, ray.dir)
    return nil if denom > 0
    dist = (Vector.dot(@_norm, ray.start) + @offset) / (-denom)
    Intersection.new(self, ray, dist)
  end
end

abstract class Surface end

class ShinySurface < Surface
  def diffuse(pos); Color_white end

  def specular(pos); Color_grey end

  def reflect(pos); 0.7 end

  def roughness; 250 end
end

class CheckerboardSurface < Surface
  def diffuse(pos)
    return Color_white if ((pos.z).floor + (pos.x).floor) % 2 != 0
    Color_black
  end

  def specular(pos); Color_white end

  def roughness; 250 end

  def reflect(pos)
    return 0.1 if ((pos.z).floor + (pos.x).floor) % 2 != 0
    0.7
  end
end

There was a big discussion about this too in Ruby, which with 3.0.0 now allows
endless methods (oneline method definitions that don’t need the end keyword).

I think adding this to Crystal would have many benefits too.

  1. It would allow compatibility with Ruby 3+ going forward.
  2. It would make source code much shorter, and easier to read/write/maintain.
  3. It would be simpler to read for newbies coming to the language.
  4. It could possibly encourage more concise thinking and refactoring.
  5. It wouldn’t affect any legacy code.

The only possilbe “con” I can think of (beside having to change the parser to
accommodate the change), is the emotional resistance to it (see Ruby’s discussions).

Here is what those code examples would look like using endless methods, using Ruby’s syntax. Too me, this is not only more beautiful code, but is concise, very easy to understand, and easier to write and read, and eliminates allot of unnecessary syntactical code noise.

class Sphere < Thing
  @radius2 : Float64

  def initialize(@center : Vector, radius : Float64, @_surface : Surface) = @radius2 = radius*radius

  def normal(pos) = Vector.norm(Vector.minus(pos, @center))

  def surface = @_surface

  def intersect(ray)
    eo = Vector.minus(@center, ray.start)
    v  = Vector.dot(eo, ray.dir)
    dist = 0.0
    if (v >= 0)
      disc = @radius2 - (Vector.dot(eo, eo) - v * v)
      dist = v - Math.sqrt(disc) if (disc >= 0)    
    end
    
    return nil if (dist == 0)

    Intersection.new(self, ray, dist)
  end
end

class Plane < Thing
  def initialize(@_norm : Vector, @offset : Float64, @_surface : Surface) end

  def normal(pos) = @_norm

  def surface = @_surface

  def intersect(ray)
    denom = Vector.dot(@_norm, ray.dir)
    return nil if denom > 0
    dist = (Vector.dot(@_norm, ray.start) + @offset) / (-denom)
    Intersection.new(self, ray, dist)
  end
end

abstract class Surface end

class ShinySurface < Surface
  def diffuse(pos) = Color_white

  def specular(pos) = Color_grey

  def reflect(pos) = 0.7

  def roughness = 250
end

class CheckerboardSurface < Surface
  def diffuse(pos)
    return Color_white if ((pos.z).floor + (pos.x).floor) % 2 != 0
    Color_black
  end

  def specular(pos) = Color_white

  def roughness = 250

  def reflect(pos) = ((pos.z).floor + (pos.x).floor) % 2 != 0 ? 0.1 : 0.7 
end

This was brought up before, and ultimately rejected iirc.

Thanks, I wasn’t aware it was formally requested before.

However, that discussion was in April 2020, before Ruby 3.0.0 was released on December 25, 2020, which does include it.

I understand from a devs perspecitve, it may be seen as allot of work to create an unnecessary feature (life will go on with or without it). However, for the reasons stated in the Crystal discussion on it, and the longer discussions in the Ruby community, that ultimately won over Matz to include it, it is definitely a winner for the programmer writer, and code readers.

Now that Crystal has hit 1.0.0, I would like it to be reconsidered as a future feature, now that the language has reached a point of significant stability.

I think those are very different things sometimes.

One example is Ruby having method aliases whereas Crystal discourages them.

So Ruby is optimized for writers. Coming from different languages makes you feel at home when you try some method name and it works. For the same reason it would hurt other people reading the code with different names for methods used by different people.

Crystal on the other hand is optimized for readers. It would make people learn the proper name for method coming from different language (harder to write - you have to learn the name of method in Crystal), but then reading is easier as there is only one name for method and people wouldn’t be confused by seeing method detect and wondering what it does. Oh, it’s the same as select?

3 Likes

I don’t think what you name methods is relevant to this discussion here, which is about language syntax for defining methods.

It may be helpful to see the Ruby discussions leading to its inclusion. They mirror most of the issues raised in the Crystal discussion.

https://bugs.ruby-lang.org/issues/16746

copy from kotlin

I don’t like the idea of Crystal becoming indent sensitive (seems like it would open up potential formatting typo issues, too easy to overlook). But a block-like alternative format would be fine with me; as shown in https://bugs.ruby-lang.org/issues/16746:

# original
def value; @val; end

# proposed - is that conflicting one?
def value { @val }
1 Like

Endless method is not indent sensitive

1 Like

name= is setter

Parentheses for method definitions are required in Crystal so there’s no amboguity.

def setter=foo is a getter
def setter=(foo) = @foo = foo is a setter

Regardless, this was discussed in the past and rejected.

This only adds a new syntax for method definitions, so it’s not a big feature enabler. It also adds more ways to do the same thing so people will start discussing styles even more, which is just a waste of time.

4 Likes

Gee @asterite, you seem to have changed your opinion from the April 2020 discussion, where you were more considerate.

As I said in raising this issue, the greatest resistance will be emotional and not technical.
But try to look at it solely from the perspective of the (potential) user.

Technically, it’s a trivial change. You clearly set the syntax for doing it correctly, and its done. Like with everything else, if the user messes it up the compiler will tell them.

So let’s get to the larger, and more important, factors of resistance.

A) It’s just a “style” thing.
You ignore style at your own peril.

There are libraries full of books, and total academic and industrial fields, devoted to how to make people want to buy your product. And after products get to a certain point of technical equality, what differentiates one car, hammer, shoe, software language from another is how it makes the (potential) user feel. That’s why you can get the same thing in different colors, textures, and sizes.

Scala, Kotlin, and now Ruby have it because 1) it was simple enough to do, 2) it makes them easier|faster for their users to read|write code, and 3) their users wanted it. And if that’s a little stylistic thing that will attract more users then it’s worth doing.

B) We don’t have to do everything Ruby does! :unamused:
OK…breathe. I know there’s allot of pride (ego), control, and ideology when someone(s) creates anything, especially a software language (or a Linux distro). There’s a reason a person feels their thing is “better”, or why do it (other just for pure personal fun). But it would be foolhearty not to objectively look at what others are doing in your same domain of interest, and understanding why things are being done certain ways, and where the winds of change are blowing. It doesn’t mean you have to immediately jump on these bandwagons, but you need to acknowledge the trends, and prepare to adapt. Even Microsoft is now forced to “act” friendly with Linux, because they have to to build their Cloud business using it.

C) Parents don’t control their children after they grow up.
This is a big one for creators to deal with, but will have to at some point.

When you have children, they’re not yours (your property). Your job is to get them to a point where they become functional members of the species that you don’t have to provide for at some point. The animal kingdom exhibits this quite well, which we should learn more from.

For especially an open source project, once it becomes ‘‘stable’’, and creates a community around it, and users start to depend on it, it no longer belong to its creator(s). It belongs to its users. And the users will (should) ultimately determine its future, because all parents|creators die.

Since Crystal now has shouted to the world, hey yall, we’s stable and ready to rumble, users will (have already) start requesting new features, usually from languages they’re familiar with. Users want to play with your child in ways you’d never imagine, and disapprove of maybe in some cases. But remember, you can only control your child for so long, and then you have to accept you need to set it free.

This is what happened with Perl, Python, Ruby, Linux, et al. The communities around these technologies will determine what they will become, for good and for bad.

So the issue of whether to, or not, provide endless methods will become a distant triviality compared with the decisions that will need to be made concerning Crystal’s future growth. So the more comfortable and flexible the devs are at allowing the language to become what’s its users want, the faster it will grow, and the more loved it will become.

Good points. In particular, I found point B more persuasive about why we should look to Ruby than what I’ve understood from your arguments on that point in the past. C isn’t really a relevant point to me, since I’m barely an active Crystal developer (I rarely use it at work, and I don’t have any very active side projects), let alone a designer of the language.

A, though, is mostly where I disagree. I do think you’re right that if this change were 1) simple enough to do, 2) easier and faster for users to read and write code, and 3) widely desired, it would be the right move to include it. I can’t speak to whether it’s easy, and I don’t personally want it (but clearly at least several people do), but the reason I don’t want it is that I think it would make Crystal code harder to read, and I don’t think it provides enough of a benefit on the writing end to justify that. You’re removing an end line and putting your (presumably one-liner) body into the declaration line, but that’s also cluttering up your declaration line and creating a whole new method syntax to learn. Overall, that feels like a net negative change on ease of reading. It’s quicker to write, to be sure, but typing time is by far the minority of software work.

I also just don’t think it reads well. One of the benefits of well-written (yes, subjectively) Crystal is that it tends to say what it’s doing fairly clearly in English*. To me, that’s a huge benefit from a maintenance perspective, and while I don’t think this is a huge hit to that, it seems to me like a step away from it.

* I’m a native English speaker, so I lack the perspective on how valuable this actually is to non-native speakers.

1 Like

Just a note, a one-line method can already be written without semicolons.

# Valid syntax
def add(x, y) x + y end
4 Likes

Interesting. Didn’t know that.

OTOH crystal tool format would change that into multiline, so not very usable if you are to follow standard formatting.

Also the def add(x, y) x + y; end will also be changed into multiline, same results.

Quoting this to make it more visible. This is exactly how I feel about this proposal.

1 Like

maybe this is why ruby is slower and slower.

1 Like

I’m just posting this because I think it’s an interesting take on endless methods use cases.

You could just define a simple macro and it would be even shorter than it would with endless methods. Ultimately not worth it IMO :person_shrugging:.

A custom macro would be custom magic sauce. Endless methods are also some magic sauce but more common :person_shrugging:

Another blog on this topic in Ruby: