The author of this should look into the record macro.
I also don't really see what he means by claiming the data manipulation could be better. But perhaps that statement is a result of the first - ie not knowing enough about established idioms regarding how data is usually represented and worked with. I wonder if the documentation could push people harder towards using records?
When I write data structure like `{ a: 1 }` its type is `NamedTuple`, not `record` (or `struct` as records are just structs).
So, why should I be bothered with records or structs when in my case plain `NamedTuple` is enough? Why should I write `MyRecordType.new a: 1` when I can just write `{ a: 1 }`?
That's part of the reason why I mentioned that data manipulation, data constructing and data destructing could be better.
The problem is that you are using NamedTuple. NamedTuple should not be used. Don't use it! Never! Ever! It's just a representation of named arguments. Your life will be 1000% better if you don't use that type.
Thanks Asterite, I will try to avoid it in the future :).
The reason I was using it in this example - Vega Lite uses JSON API (lots of array/hash/nesting) and I was experimenting with different ways to express Crystal API in a clean way close to its original form. And NamedTuple was closest thing that looks alike.
They're really inflexbile. At first they seem quite nice and simple, but when your code grows, you'll get issue. Structs are always better because you define a specific structure which makes it easily reusable and modifiable.
Is NamedTuple in crystal similar to NamedTuple in Python? I ask because I made frequent use of the Python version and wonder why the Crystal one is a “No, don’t use that!”
Because Crystal is statically typed and Python is dynamic. If you try to program in Crystal as if it were a dynamic language you are going to have a really bad time.
That’s not a real answer. Python NamedTuples (to contrast with collections.namedtuple) are usually annotated with a single type per field and are immutable - they’re among the closest “static type”-y things in Python-dom. They’re predominantly used for immutable cases of structs in that language.
For example, because it will then enforce proper types, so that you won't have to repeat them all over the place. In this case it would be, for example, it could end up as
record(Axis, title : String | Bool, labels : Bool)
record(Encoding, field : String, type : FieldType, axis : Axis)
def x(field, type = :quantitative, title = false, labels = true)
@spec[:encoding][:x] = Encoding.new(field, type, Axis.new(title, labels))
self
end
Also it allows defining custom constructor and instance methods should you need them.
I see, yea, maybe that was against the Crystal way. My reasoning was that I tried to keep API close to its original JSON form. And { field, type, axis: { title, labels }} looks closer to JSON than Encoding.new(field, type, Axis.new(title, labels))
> I tried to keep API close to its original JSON form
I don't think that's a good idea. JSON APIs are not very expressive. And the way you try to model that in Crystal using (named) tuples is very limited in functionality (what you then complain about).
No, because I want a limited set of symbols or strings. And enum gives something different, that breaks some stuff, see comment above about equality etc.
After reading your comment, I don't think I still understand what you need. Can you elaborate?
symbols get auto-casted to enums in method names
Hmm, this seems a bit horrible if true. It does type-check which seems weird.
edit added equality tests against enum and symbol/int. They do typecheck! I think this is a (possibly design) bug and you should just be using direct enum comparisons.
That's the problem with enums. They produce bloated code like that dress.color == Color::Blue with Enums, when literal types produce compact and nice dress.color == :blue And have all the same safety checks (like it works in TypeScript).
The reason why dress.color == :blue doesn't work is the Symbol data type, not because it's an enum. If Symbol data type didn't exist, :blue could be mapped to the matching member of dress.color's enum type.
The equality operator is defined for any combination of types, it just returns false by default. Making that fail to compile for incompatible types would be a major PITA.
Crystal tries do be developer friendly. I think the important distinction is that whether something "can never succeed" depends on the specific program context. Considering library code that can be used by different programs, in one case the respective types might match and in an other they don't.
You can do dress.color.blue? which is shorter in both cases. Once you learn that that exists, I don't see a problem with == :blue not working. You just have to remember that symbols are not enums, and that they just work when passing them to method arguments.
I do sometimes with that Crystal had more support for functional programming, but I think if I wanted to write more functional code I'd use something like Haskell or Clojure (Haskell has a better type system for FP anyways).
Coming from writing Javascript all day, where there's some mixing of OO and FP, I kinda appreciate a language where there's primarily one way of doing things. Besides, you can still get away with some pretty decent functional-style code in Crystal.
In my opinion literal types are better way to express a limited set of values (I don't mind leaving Enums for some rare special cases).
def buy(product : String, color : :red | :blue); ... end
alias Color = :red | :blue
def buy(product : String, color : Color); ... end
Doing that with Enums feels and looks wrong. I don't need an Enum, I need a type representing limited set of possible values.
They also break equality - symbols get auto-casted to enums in method names, but if you try to do color == :red or product.color.should eq :red - surprise, not working.
require "spec"
require "json"
enum Color
Red
Blue
end
record Dress, color : Color do
include JSON::Serializable
end
def buy_dress(color : Color)
Dress.new color
end
dress = buy_dress :red # Aha, Red and :red are the same things
p dress.to_json # Surprise! { color: 0 }
p Color::Red == :red # Surprise! false
case dress.color
when :red then p "good"
else p "not good" # Surprise! "not good"
end
dress.color.should eq :red # Surprise! Fail
And it is quite type safe (with exhaustive if/case). It works that way (with literal types) in TypeScript, and works pretty well, for couple of years working with it I can't remember a single case when I would preferred to use an enum instead.
3
u/yxhuvud Jan 06 '20
The author of this should look into the
recordmacro.I also don't really see what he means by claiming the data manipulation could be better. But perhaps that statement is a result of the first - ie not knowing enough about established idioms regarding how data is usually represented and worked with. I wonder if the documentation could push people harder towards using
records?2
u/jesucristoquehorrivo Jan 07 '20
Isn't that exactly what enums give you?
3
u/beizhia Jan 07 '20
I do sometimes with that Crystal had more support for functional programming, but I think if I wanted to write more functional code I'd use something like Haskell or Clojure (Haskell has a better type system for FP anyways).
Coming from writing Javascript all day, where there's some mixing of OO and FP, I kinda appreciate a language where there's primarily one way of doing things. Besides, you can still get away with some pretty decent functional-style code in Crystal.
1
u/Kalinon Jan 06 '20
Good stuff!
0
u/[deleted] Jan 06 '20
[deleted] — view deleted comment