Open dmgr opened 2 years ago
Hi! Could you provide some example code of what you mean? Functions are first class citizens in Crystal. That's Proc.
I would like to see that syntax possible:
operation = Math.sin
2.then(operation)
instead of the current approach:
operation = Math.method(:sin)
2.then(&operation)
which will encourage community to write a neat functional code.
You are describing Ruby but this repository is about Crystal programming language. Are you sure you're in the right place?
I would like to see Crystal as a better Ruby. As Crystal is Ruby inspired language, it can deviate from it. So I would like Crystal has first-class functions also for methods. If Crystal would be designed with it from the beginning, it would be a very sexy language in my opinion.
You must provide the parameter types yourself:
struct Float64
def then(fn : Float64 -> T) : T forall T
fn.call(self)
end
end
Math::PI.then(->Math.sin(Float64)) # => 1.2246467991473532e-16
->Math.sin
alone would have produced some kind of method object rather than a Proc
due to the possibility of overloading, in which case the benefits of FP quickly vanish. (It may look like this particular example could be improved by a new form of autocasting, but it isn't as simple as that.) It is actually harder than in Ruby where overloads do not exist outside RBS.
Also the standard library does not have a commitment to support FP beyond e.g. the higher-order methods in Enumerable
. There are probably shards out there that do this for you.
Also, and this is just my opinion, I think it's really bad design if you write Math.sin
and that gives you a function instead of telling you "Hey, you are missing arguments." It's one of the gripes I have with many functional programming languages. It leads to really bad error messages.
I prefer the explicitness of Ruby (and Crystal.)
It is not "explicitness of Ruby (and Crystal)".
You can rather say otherwise: it is the explicitness of functional languages that they require you to explicitly use parenthesis if you want to call the function.
That's not the case in Haskell nor Elm.
What language are you thinking about?
Julia is the language I am thinking about: https://rosettacode.org/wiki/First-class_functions#Julia
What if Math.sin doesn't take arguments? Would that be a call to that method, or it would return the function that you can later call using parentheses?
I think your proposal in the end requires always using parentheses for calls, and never using parentheses when you don't want to call something. That would be a huge breaking change.
I don't think this change will ever happen.
See also: https://github.com/crystal-lang/crystal/issues/8591
The concrete proposal to let the method name without parentheses refer to the method itself would be too much of a breaking change for Crystal and Ruby likewise.
But I suppose it wouldn't hurt to talk about alternative ideas for facilitating a more functional programming style in Crystal?
Would it be possible to have a way to turn such a feature on/off somehow, for example using a pragma:
# functional_programming: true
or a block directive?:
with :functional_programming do
operation = Math.sin
2.then(operation) # it will be 2.then(&operation) in this case as #then expects block
end
or a refining kind of expression:
using FunctionalProgramming
operation = Math.sin
2.then(operation) # it will be 2.then(&operation) in this case as #then expects block
This way all the current will work as is and when you want to use a more functional programming style (for example in a data science projects) you will have a way to turn it on.
It will lead to more changes in that scopes, for example to let you call lambdas using parentheses only:
using FunctionalProgramming
multiply = ->(a) { ->(b) { a * b } } # or even more simplified way: multiply = ->(a) ->(b) { a * b }
multiply(2)(3) # => 6
# currently this works:
multiply[2][3] # => 6
multiply.(2).(3) # => 6
multiply.call(2).call(3) # => 6
So referring back to https://rosettacode.org/wiki/First-class_functions#Ruby
The code at the end could looks like this in Crystal:
using FunctionalProgramming # turning "always use parentheses for calls" mode on
cube = ->(x) { x**3 }
croot = ->(x) { x**(1.0/3.0) }
# currying function:
compose = ->(f,g) ->(x) { f(g(x)) } # or ->(f,g)->(x) { f g x }
funclist = [Math.sin, Math.cos, cube]
invlist = [Math.asin, Math.acos, croot]
puts funclist.zip(invlist).map { |f, invf| compose(f, invf)(0.5) }
How much experience do you have with Crystal? Are you using it in production? Any hobby project you can share?
The snippet for Ruby is itself an example that you don't need first-class functions to be able to write FP-oriented code, because Ruby too only has methods, not first-class functions. This is an example of mimicking it in Crystal (with full currying):
macro def_functor(name, &block)
{% for param, i in block.args %}
{% type_params = (0...i).map { |j| "T#{j}".id } %}
record Func_{{ name.id }}_{{ i }}{% if i > 0 %}({{ type_params.splat }}){% for t, j in type_params %}, {{ block.args[j] }} : {{ t }}{% end %}{% end %} do
def []({{ param }})
{% if i < block.args.size - 1 %}
Func_{{ name.id }}_{{ i + 1 }}.new({{ block.args[0..i].splat }})
{% else %}
{{ block.body }}
{% end %}
end
def [](arg, *rest : _)
self[arg][*rest]
end
end
{% end %}
def self.{{ name.id }}
Func_{{ name.id }}_0.new
end
end
module Foo
def_functor(cube) { |x| x ** 3 }
def_functor(croot) { |x| x ** (1.0 / 3) }
def_functor(compose1) { |f, g, x0| f[g[x0]] }
module Math
def_functor(sin) { |x| ::Math.sin(x) }
def_functor(cos) { |x| ::Math.cos(x) }
def_functor(asin) { |x| ::Math.asin(x) }
def_functor(acos) { |x| ::Math.acos(x) }
end
funclist = [Math.sin, Math.cos, cube]
invlist = [Math.asin, Math.acos, croot]
puts funclist.zip(invlist).map { |f, invf| compose1[f, invf][0.5] }.join('\n')
end
Anything larger than that amounts to reimplementing the entire standard library.
The main ingredients are there. This is the working version of the code above:
cube = ->(x : Float64) { x**3 }
croot = ->(x : Float64) { x**(1.0/3.0) }
compose = ->(f : Float64 -> Float64, g : Float64 -> Float64) { ->(x : Float64) { f.call(g.call(x)) } }
funclist = [->Math.sin(Float64), ->Math.cos(Float64), cube]
invlist = [->Math.asin(Float64), ->Math.acos(Float64), croot]
puts funclist.zip(invlist).map { |f, invf| compose.call(f, invf).call(0.5) } # => [0.5, 0.4999999999999999, 0.5000000000000001]
There are a couple of things worth pointing out (I take Scala as a reference):
->Math.sin(Float64)
). I don't think there's much debate here.g.call(x)
) is not terrible. But maybe we could look into having a .()
class/instance method to handle such things.The real deal is with having to type all of the arguments, and the corresponding lack of generics in procs.
If we were capable of substituting generic types in procs, the code would be definitively nice. This said, note that cube
and croot
still need the type. But compose
's types could in theory be guessed:¹
cube = ->(x : Float64) { x**3 }
croot = ->(x : Float64) { x**(1.0/3.0) }
compose = ->(f, g) { ->(x) { f.call(g.call(x)) } }
funclist = [->Math.sin(Float64), ->Math.cos(Float64), cube] # We need to know what cube
is!
invlist = [->Math.asin(Float64), ->Math.acos(Float64), croot]
f
and invf
can be inferred, and so compose
.puts funclist.zip(invlist).map { |f, invf| compose.call(f, invf).call(0.5) } # => [0.5, 0.4999999999999999, 0.5000000000000001]
¹ Don't quote me on this!
I'd suggest reviving #9197
The concrete proposal to let the method name without parentheses refer to the method itself would be too much of a breaking change for Crystal and Ruby likewise.
But I suppose it wouldn't hurt to talk about alternative ideas for facilitating a more functional programming style in Crystal?
Was working on porting some PHP code and noticed they have https://www.php.net/manual/en/functions.first_class_callable_syntax.php now in 8.1 which allows you do do $someObj->someMethod(...)
to create an anonymous function from a callable similar to Crystal's ->some_obj.some_method
.
I'm not saying that's something we should or should not have, but I did want to bring up some of the features it allows. Mainly that it allows retaining the class/method name used to create the function. In crystal when you use ->some_method
you lose all that information.
EDIT: Accessible via reflection, not directly on the closure.
I also recently learned the proc exposes its closured data (the object), but in a seemingly not robust/unsafe way? Given I would have expected the ID/object ID of each instance to be the same since the proc would closure a reference to the original object?
class Test
@id : Int32 = 0
def on_message : Nil
puts "foo"
end
end
t = Test.new
t # => #<Test:0x7efd374acea0 @id=0>
proc = ->t.on_message
proc.closure_data.as Test # => #<Test:0x7efd374abff0 @id=32509>
It might be worth either fixing or documenting this as it could be a pretty slick way to retain access to information from the original object.
There seems to be some misconception about closure_data
. It's not a direct pointer to some closured value as you seem to expect. That closure_data.as(Test)
cast is just nonsense.
closure_data
is to be treated as an opaque pointer to the closure context. You can't read anything from it directly (I guess technically there would be some way to do that, but it's quite a bit complex).
I was able to get it to work with some help on Discord:
record MyMessage
class Test
property id = 10
def on_message(message : MyMessage) : Nil
puts "foo"
end
def test : Nil
@id += 20
end
end
t = Test.new
pp t # => #<Test:0x7f862979dea0 @id=10>
proc = ->t.on_message(MyMessage)
proc_t = proc.closure_data.as(Test*).value
proc_t.test
proc_t.id += 10
pp t # => #<Test:0x7f862979dea0 @id=40>
However based on what you say, it seems like it's just a coincidence that it works like this? If so, should this method even be public?
should this method even be public?
closure_data
can be used to make Crystal closures integrate with C APIs. It is very common for C APIs that accept a function pointer to also accept a void*
data pointer, that is passed back to you when the function is called. Thus you could reconstruct the closure proc with the corresponding Proc#new(pointer, closure_data)
method.
I think it would be interesting to see a Ruby inspired language with methods as first-class functions support which let you write functional oriented programming a breeze. Of course it will affect syntax as you will be required to use parenthesis to call a function, but I tend to use them anyway. This is a feature I miss in Ruby.
That feature could be introduced by a directive on the beginning of a
.rb
file, similar how it is done with# frozen_string_literal: true
. So a directive like# first_class_functions: true
can be used.TL;DR
I would like to see that syntax possible:
instead of the current approach: