d2jsp
Log InRegister
d2jsp Forums > Off-Topic > Computers & IT > Programming & Development > Crystal-lang Fan Thread
Prev12
Add Reply New Topic New Poll
Member
Posts: 13,425
Joined: Sep 29 2007
Gold: 0.00
Warn: 20%
May 5 2016 07:53pm
Version jump to 0.16.0 today with a lot of language breaking changes to benefit the language in the future.

First is that now all global, class and instance variables have to by statically typed, they are no longer inferred by the compiler. This reduces memory usage while compiling, increases compiling speed, and brings the language one step closer to incremental compilation.

Next it now officially supports freebsd and musl libc support along with the side effect that it is now a bit easier to port the language to other platforms.

Continuing on, there are now named arguments in which you can specify which argument you want to use in any order.

For instance:

Code
def method(a, b, c)
a+b+c
end


Can be called like this or this:

Code
method(10,20,30)
method(c:30, a:10, b:20)


This makes code easier to read for methods which have very specific data such as the oauth2 client:

Code
require "oauth2"

# Option 1
client = OAuth2::Client.new(
"some_host",
"some_client_secret",
"some_client_id"
)

# Option 2
client = OAuth2::Client.new(
host: "some_host",
client_secret: "some_client_secret",
client_id: "some_client_id"
)


Moving along, we now have BigFloats and BigRational support which should be enough for math heavy use along with BigInt.

We now also have binary searching built into ranges and arrays. For example, we can solve x^3 + x^2 + x - 2:

Code
answer = (-Float64::INFINITY..Float64::INFINITY).bsearch { |x| x ** 3 + x ** 2 + x - 2 >= 0 }
puts answer # => 0.810536


JSON and YAML now support mapping to BigInt and BigFloat.

And then a bunch of misc stuff. We now have a slice method, a each_line iterator for files, reverse_each for ranges, insert for strings, and a bunch of other things.

Check out the change log for more information: https://github.com/crystal-lang/crystal/releases/tag/0.16.0

This post was edited by AbDuCt on May 5 2016 07:54pm
Member
Posts: 13,425
Joined: Sep 29 2007
Gold: 0.00
Warn: 20%
May 21 2016 08:45pm
Version 0.17.0 is out with some new features:

Turples:

Basically they are immutable compile-time equivalents of arrays. They cannot be changed or modified during run time, but this allows for them to be held on the stack removing the need for heap allocations.

Code
turple = {2, "wow"}
tuple[0] # => 1 (compile-time type is Int32)
tuple[1] # => "hi" (compile-time type is String)


Named Turples:

Same thing as above but with names! You can think of these as stack based compile time immutable hashes.

Code
tuple = {foo: "hello", bar: 2}
tuple[:foo] # => "hello" (compile-time type is String)
tuple[:bar] # => 2 (compile-time type is Int32)


Splats and double splats:

We can now have variable number of arguments via the splat and double splat operators which use turples and named turples respectively. For instance:

Code
def foo(x, y, *other)
# Return the tuple
other
end

other = foo(1, 2, 3, "foo", "bar")
#other => {3, "foo", "bar"}


Other will hold the last three variables passed to the method since they were not defined in the methods definition. With named turples you can use the double splat operator to do something similar:

Code
def foo(x, y, **other)
# Return the named tuple
other
end

other = foo(1, z: 3, y: 4, w: 5)
other # => {z: 3, w: 5}


Since only x and y were named, the double splat named turple returned holds the z and w variables. Both versions also allow for unpacking of the turples into methods for instance:

Code
def foo(x, y)
x - y
end

tup = {10, 3}

# Here we "unpack" the tuple into arguments
foo(*tup) # => 7


Continue to part 2

This post was edited by AbDuCt on May 21 2016 08:45pm
Member
Posts: 13,425
Joined: Sep 29 2007
Gold: 0.00
Warn: 20%
May 21 2016 08:46pm
Part 2:


Overloads and new method parsing:

We now have overloads for methods. This required an algorithm change to how methods were parsed, but it opened up a larger venue for future features.

Forced named parameters can be used to overload methods in various ways. For example here are two function definitions with the same name, but different method definitions:

Code
# Two positional arguments allowed, z must be passed as a named argument
def foo(x, y, *, z)
end

foo(1, 2) # Error, missing argument: z
foo(1, 2, 3) # Error, wrong number of arguments (given 3, expected 2)
foo(1, 2, z: 3) # OK

# This is another overload: because arguments after * must be
# passed by name, they are part of a method's signature.
def foo(x, y, *, w)
end

foo(1, 2, w: 3) # calls the method above


External method names:

We also have external method names now. This makes reading APIs much easier now simply due to it being more verbose without modifying much of anything. For example here is an increment method which would be written in the old style:

Code
def increment(value, by)
value + by
end

increment(1, 2) # => 3


And now with external names we can further make method definitions clearer as well as make the method calls themselves more sensible.

Code
def increment(value, by amount)
value + amount
end

increment(1, 2) # => 3
increment(1, by: 2) # => 3


My thoughts:

I haven't installed this update yet, but I am curious as to how this functions with the last updates named arguments for passing data to methods. For instance in the above external named function would:

Code
increment(amount:2, value:1)
increment(by:2, value:1)


compile as it did in the previous versions? Guess I will need to find out.

This post was edited by AbDuCt on May 21 2016 08:48pm
Member
Posts: 16,218
Joined: Sep 27 2009
Gold: 13.00
May 31 2016 03:35am
How did they manage to outperform Ruby so much ?

"Ruby completed the task in 73 seconds and crystal completed it in 3 seconds. That was a speed reduction of 24 times, just by compiling the same ruby code with the crystal compiler. The only thing that needed changing is that in crystal they have not concluded on how they wanted to handle the types of an array/hash so they specifically have to be typed until the figure it out."

This explaination doesn't really fit me. Do you have more intels on their compiler or algorithms they used in ? I checked your most recent post where you copy / paste the comparative array between Ruby and Crystal, can't figured out how they managed to have this astonishing performance, especially given that there is no solid argumentation, just an array. Would like to have in-depths explanations.

This post was edited by Bremen on May 31 2016 03:38am
Member
Posts: 13,425
Joined: Sep 29 2007
Gold: 0.00
Warn: 20%
May 31 2016 07:05am
Quote (Bremen @ May 31 2016 05:35am)
How did they manage to outperform Ruby so much ?

"Ruby completed the task in 73 seconds and crystal completed it in 3 seconds. That was a speed reduction of 24 times, just by compiling the same ruby code with the crystal compiler. The only thing that needed changing is that in crystal they have not concluded on how they wanted to handle the types of an array/hash so they specifically have to be typed until the figure it out."

This explaination doesn't really fit me. Do you have more intels on their compiler or algorithms they used in ? I checked your most recent post where you copy / paste the comparative array between Ruby and Crystal, can't figured out how they managed to have this astonishing performance, especially given that there is no solid argumentation, just an array. Would like to have in-depths explanations.


The crystal compiler uses LLVM to compile its code to native machine code on the host system much like C. Ruby on the other hand is analyzed at run time and passed through an interpreter in order to run. The differences between being an interpreted language and a compiled language are night and day performance wise in most instances. Not only does ruby have to interpret the code, but it has to run checks on many of the calls, variables, functions, methods, and other things many times over while running. Crystal on the other hand has already done this during the compiling phase allowing it to run without the need of these basic checks. The only real similarity between it and Ruby is that it shares an "almost" identical syntax.

Some other key points are:

  • Ruby is interpreted, and the interpreter could be improved. For example other interpreted languages like JS or Java have a very good VM and JIT compiler.
  • Many Ruby checks that are done at runtime, in Crystal are done at compile time. For example a simple method call in Ruby ends up in a method lookup. Even with a cache it won't beat a native function call. Or when Ruby decides to do different things based on the type of an argument, these checks are done at runtime. In Crystal they are known at compile time so those checks disappear. Without those checks the compiler can inline calls and do some pretty crazy stuff (thanks to LLVM). Or, for example, looking up an instance varaibles is a hash lookup in Ruby (as far as I know), while in Crystal it's just a memory indirection and load.
  • In Crystal we try to avoid extra memory allocations. For example to_s(io) writes to an IO instead of converting the object to a string in memory. Or we have tuples for fixed-sized arrays that are allocated on the stack. Or you can declare a type as a struct to avoid heap allocations.
  • Calls to C are done directly, without wrappers. Well, you could have a wrapper but that will be inlined by LLVM. In Ruby it always has to resolve a Ruby method first.


Overall crystal is designed in such a way to remove much of the overhead a dynamic run time interpreter has that the original ruby was using.

edit:: The downside of this at the moment is that the native code it compiles to has to be native to the HOST. As of right now it can only compile to LINUX (and cross compiles to most arches) and MAC OSX hosts which support the LLVM and a few other libraries. Windows is not supported as of yet and probably will not be supported until a long time down the future. This is because they rather another group port it to Windows while they work on the core feature set on a OS they use more often.

This post was edited by AbDuCt on May 31 2016 07:11am
Member
Posts: 13,425
Joined: Sep 29 2007
Gold: 0.00
Warn: 20%
Jun 19 2016 10:19pm
Crystal 0.18.0 has been released (a while ago).

Updates:

Unions

These were always available, but they were not able to be accessed since they did not have a type name. Most noticeably you would have used them when declaring an hash or other type that could house many types.

Now being able to access unions directly, we can do thinks such as extract json, which usually has many data types throughout the struct, into other containers such as arrays. In this example we parse a json string and map them to a Result class which uses the unions of Point and Circle as the type definition of the variable shape. As the json is deconstructed and the mapping is done the union figures out which type the json string satisfies and automatically returns that type.

Code
require "json"

struct Point
JSON.mapping x: Int32, y: Int32
end

struct Circle
JSON.mapping center: Int32, radius: Int32
end

class Result
JSON.mapping shape: Point | Circle
end

result = Result.from_json(%({"shape": {"x": 1, "y": 2}}))
result # => Result(@shape=Point(@x=1, @y=2))

result = Result.from_json(%({"shape": {"radius": 1, "center": 2}}))
result # => Result(@shape=Circle(@center=2, @radius=1))

shapes = Array(Point | Circle).from_json(%([{"x": 1, "y": 2},
{"radius": 1, "center": 2}]))
shapes # => [Point(@x=1, @y=2), Circle(@center=2, @radius=1)]


Hashes, Enumerables, and block unpacking

Crystal now has an Enumerable class which adds many methods which allow you to iterate an object on an object by object manner. This is similar to a "for each" loop in other languages, but instead exposes the map, and different flavors of the each method like ruby.

For example we can now enumerate a hash retrieving the key, value pairs in separate local scope variables:

Code
hash = {1 => "a", 2 => "b"}
hash.each do |key, value|
# Prints "1: a", then "2: b"
puts "#{key}: #{value}"
end

hash.map { |key, value| "#{key}: #{value}" } # => ["1: a", "2: b"]


Splats in yield and block arguments

We can now give splat arguments to blocks and yields allowing us to easily pass on block arguments to other methods. This allows you to forward blocks of code to other methods which can in turn yield results. I am not sure if I would ever try using this.

Named tuples and arguments creation with string literals

I am not actually to fond of this change since I think it looks weird, but it is useful none the less. One particular example is the ability to create json objects with keys that have spaces (before I don't think it was possible unless you used a hash which is slower than turples for reasons outlined in previous updates).

Code
require "json"

{"hello world": 1}.to_json # => "{\"hello world\":1}"


Class variables are now inherited

I welcome this change as it brings classes a little bit closer to being more ruby like. Now Class variables (denoted with @@, rather than instance variables denoted as @) are inherited into subclasses, but each subclass can keep independent values.

In this example we have a class foo with a class variable. Bar inherits from foo also getting an exact copy of the variable. Although they share exact copies now, each class variable are able to work independent of each other:

Code
class Foo
@@value = 1

def self.value
@@value
end

def self.value=(@@value)
end
end

class Bar < Foo
end

p Foo.value # => 1
p Bar.value # => 1

Foo.value = 2

p Foo.value # => 2
p Bar.value # => 1

Bar.value = 3

p Foo.value # => 2
p Bar.value # => 3


Notable mentions

  • OpenSSL and TLS improvments
  • Functional language generics for user defined classes
  • Other fixes to the standard library and error messages


This post was edited by AbDuCt on Jun 19 2016 10:20pm
Go Back To Programming & Development Topic List
Prev12
Add Reply New Topic New Poll