Skip to content

Gotchas For Crystalers and Rubyists #5

Open
@ozra

Description

@ozra

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

  • Symbols are called Tags 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() than MyMod::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 declared trait MyTrait, not module MyTrait, and mixed in to a type with mixin MyTrait - not include

  • 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: .|., .&., .^., .~. - 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]. - this will change back in a push not to soon...

  • 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 called init
    • self does not refer to the current type, you use Self or Type (capitalized). One of the terms may be ditched.
    • "class-methods" are declared as Type.my_method(params) -> (or Self...) and are currently referred to as Type Level Functions.
    • class-variables can be accessed either as @@class–var or Type.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 write foo = 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 or fn, however the idiomatic way is just my–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 named try/rescue/fulfil/ensure.

  • fulfil is the same as else 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!

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions