Description
Gotchas For Crystalers and Rubyists
It's reasonable that some of you who find interest in Onyx know a bit of Crystal.
There are some differences that can be confusing, likewise if you come from a Ruby background.
-
Indentation matters!
-
end
keywords are optional, but significant when used, and can also be more explicit:end-type
-
Comments are written
-- comment here
-
Symbol
s are calledTag
s and are written#symbol_name
, not:symbol_name
(think hash-tag) -
Char-literals are written
%"a"
not'a'
- they're not too common except in special code bases. -
Dont use
::
for namespacing / module-hierarchy-digging, simply use dot:MyMod.IsMuch.More.Natural.ToUse.like-this()
thanMyMod::IsMuch::More::Natural::ToUse.like-this()
-
Type annotating variables, instance-vars, parameters etc. is done simply by writing the type after the identifier, spaced. No colon or double colons. There are also some type modifier/qualifier symbols, but they're in early workings.
foo SomeType = 47 -- foo typed to SomeType and assigned 47
-
It's recommended to use endash or hyphen as word-delimiter, however the archaic underscore-style is allowed interchangeably, even camel-case can be used (but don't!).
-
a
module
is a module, a "type-partial" is declaredtrait MyTrait
, notmodule MyTrait
, and mixed in to a type withmixin MyTrait
- notinclude
-
do
is a generic "nesting starts" keyword, not a "crystal/ruby-block" starter.do
,then
,:
,=>
are exactly equivalent. Example:if foo then "ok"
,if foo => "ok"
,if foo: "ok"
, etc. -
A "rubyish block" is called "soft-lambda" (please, help with a better term!), because frankly "block" is too generic, ambiguous and confusing as a term. Soft-lambdas begin with
(params, here) ~>
or~>
only. Instead of&.do_stuff
, you would use the visually similar~.do_stuff
(to keep with the style of the other soft-lambda notations.) -
A "soft-lambda" is part of the arguments, not an "add-on". That means it goes inside parentheses if they're used in a parenthesized call, and is separated with a comma if there are arguments before it. It still only goes last among the arguments.
-
with '~>', auto-parametrization is available (KDB/Q inspired). Simply use names
_x
where x is the position in parameters it should represent. Count begins with 1. -
This means that in Onyx you could write (a few variations here, result unused):
list.map (v) ~> v * v list.map (v) ~> v * v list.map((v) ~> v * v) list.map (v) ~> v * v end -- explicit 'end' is voluntary list.map ~> _1 * _1 -- implicit parametrization
-
Bitwise operations are not commonplace and therefore they have been demoted to the Haskellish form:- this will change back in a push not to soon....|.
,.&.
,.^.
,.~.
- that is, simply surrounded with dots (think bits...). Note that the same goes for any type overloading those operators: List "unique join" would also be[1, 2] .|. [2, 3]
. -
Generic types / type constructors. In Crystal you use
Type(Type2(Type3))
, in Onyx you can currently use<
/>
and[
/]
, like so:Type[Type2<Type3>]
. -
Instantiation of types has short form
foo = Foo("Param to init")
which is the same as calling.new
. Parentheses has to be used if there are no arguments:foo = Foo()
, otherwise it just represents the type (unlike first-hand callables that are calls primarily and values only when specified). Another example with generics:list = List[String]()
. The short form is preferred instead of calling new. -
Lambdas are defined as funcs, but without name, or rephrased: like soft-lambdas but with straight arrow, so also here it might look a bit backward to Crystalers:
foo = (x I32, y I32) -> x + y
-
Lambdas and any instances having 'call'-methods defined can be called Functor-style, that is, used as functions directly. They primarily represent their value, so if arg-less call is wanted, empty parentheses are needed.
my-lambda = (x Str) -> say "x: {x}"; end; my-lambda "47"
-
String interpolation is simply written
"{interpolate-me}"
, not"#{...}"
-
and
,or
,is
,isnt
,not
is available in addition to&&
,||
,==
,!=
,!
- they behave exactly the same as their symbolic counterparts, it's a mere matter of lexical choice. -
for
-loops are available - they're pure sugar for.each*
-iteration, and so scoping and behaviour exactly mimics method based soft-lambda iteration. A multitude of syntactical variations are currently available, whatever you find natural to use will likely work. The abundance of choices will be reduced by consensus. -
In type-defs:
initializer
is simply calledinit
self
does not refer to the current type, you useSelf
orType
(capitalized). One of the terms may be ditched.- "class-methods" are declared as
Type.my_method(params) ->
(orSelf
...) and are currently referred to as Type Level Functions. - class-variables can be accessed either as
@@class–var
orType.class–var
. These also, are called Type Level Variables. I'm inclined to ditch the@@
-notation. - instance-vars do not need
@
prefix at declaration site. - instance-vars can be typed, default assigned and getter/setter flagged on the same line.
-
Integer literals are of type
StdInt
(platform width), not Int32 by default. -
The type of int- and real literals can be changed through parser-pragma, and can therefore be retyped per scope as seen beneficial for clear code.
-
There's no ternary conditional in the common form, it is prefixed with
if
. Or, better phrased:if
has an alternative nest-start/else notation:foo = if bar is 47 ? 1 : 2
. You could as well writefoo = if bar is 47 then 1 else 2
-
There are many ways to write
case
constructs currently, try them out and don't get stuck with what you're currently used to - you might find something you like more. You can currently write them exactly as in Crystal. The huge amount of styles now available will be reduced based on RFC input. -
func/method defs can currently be written prefixed with
def
orfn
, however the idiomatic way is justmy–def(params) -> body
. It's uncertain whether the keyword style will remain. -
splat parameter is written
...par–name
- not*par–name
-
a routine / func that only mutates can be written with an exclamation mark after the arrow to automatically set return type Nil, and value, to not leak internal state by mistake:
my-evil-func(x) ->! x.bar = 47 -- returns nil
-
begin
/rescue
/else
/ensure
constructs are namedtry
/rescue
/fulfil
/ensure
. -
fulfil
is the same aselse
in Crystal but can be used even when there's no begin/rescue defined - to ensure that something always run on successful return of a function, but not if it raises and is caught by some other spot up the call chain. -
"Attributes" are called "Pragmas" - simply because they are pragmatic constructs that differ in semantics from use-case to use-case. The syntax currently is an apostrophe followed by pragma:
'link("some_lib")
. The names differ from Crystal too. See issue on pragmas.
Probably some more things - help out by commenting on what confuses you!