Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for defining additional methods #1

Merged
merged 1 commit into from
Dec 26, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
58 changes: 44 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@


# dataclass

A [Crystal](http://crystal-lang.org/) macro to ease the definition of *data classes*, i.e. classes whose main purpose is to hold data.

Data class instances are immutable, and provide a natural implementation for the most common methods.
Expand Down Expand Up @@ -74,6 +75,7 @@ p.copy(age: p.age + 1) # => Person(Rick, 29)


### Pattern-based parameter extraction

Data classes enable you to extract parameters using some sort of pattern matching. This is powered by a custom definition of the `[]=` operator on the data class itself.

For example, given the data classes
Expand Down Expand Up @@ -106,6 +108,7 @@ Skipping the initialization step will produce a compilation error as soon as you


### Destructuring assignment

Data classes support destructuring assignment. There is no magic involved here: data classes simply implement the indexing operator `#[](idx)`.

```crystal
Expand Down Expand Up @@ -152,29 +155,64 @@ The `dataclass` macro supports type parameters, so the following code is valid
dataclass Wrapper(T){value : T}
```

### Support for defining additional methods

The `dataclass` macro supports defining additional methods on your data class. If you pass the dataclass macro a code block, the body of the code block will be pasted into body of the expanded class definition.

```crystal
dataclass Person{name : String} do
def hello
"Hello #{@name}"
end
end

Person.new("Matt").hello # => "Hello Matt"
```

This also works in conjunction with inheritance.

### Under the hood
The expression `dataclass Person{name : String, age : Int = 18}` is equivalent to the following:

The `dataclass` macro expands such that following definitions are equivalent.

```crystal
dataclass Person{name : String, age : Int = 18} < OtherType do
def hello
"Hello #{@name}"
end
end
```

```crystal
class Person
class Person < OtherType
getter(name)
getter(age)
def initialize(name : String, age : Int = 18)
@name = name
@age = age

def initialize(@name : String, @age : Int = 18)
end

def hello
"Hello #{@name}"
end

def_equals_and_hash(@name, @age)

def copy(name = @name, age = @age) : Person
Person.new(name, age)
end

def [](idx)
[@name, @age][idx]
end

def to_tuple
{@name, @age}
end

def to_named_tuple
{name: @name, age: @age}
end

def to_s(io)
fields = [@name, @age]
io << "#{self.class}(#{fields.join(", ")})"
Expand All @@ -183,22 +221,14 @@ end
```

### Known Limitations

* dataclass definition must have *at least* one argument. This is by design. Use `class NoArgClass; end` instead.
* trying to inherit from a data class will lead to a compilation error.
```crystal
dataclass A{id : String}
dataclass B{id : String, extra : Int32} < A # => won't compile
```
This is by design. Try defining your data classes so that they [inherit from a commmon abstract class](https://stackoverflow.com/a/12706475) instead.
* dataclass definitions are body-free. If you want to define additonal methods on a data class, then just re-open the definition:

```crystal
dataclass YourClass{id : String}

class YourClass
# additional methods here
end
```

## Development

Expand Down
22 changes: 22 additions & 0 deletions spec/dataclass_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,15 @@ end
dataclass B{id : Int32} < A
dataclass C{id : Int32}
dataclass Compound{id : String, b : B, c : C}
dataclass WithBlock{id : Int32} < A do
def foo
"bar"
end

def tick
"super: #{super}"
end
end

dataclass WithTypeParam(T){field : T}

Expand Down Expand Up @@ -162,4 +171,17 @@ describe DataClass do
else fail("should have matched the above")
end
end

it "supports defining class bodies in blocks" do
WithBlock.new(id: 10).foo.should eq("bar")
end

it "supports inheritance with block" do
WithBlock.new(id: 10).is_a?(A).should eq(true)
WithBlock.new(id: 10).is_a?(B).should eq(false)
end

it "supports calling super from block" do
WithBlock.new(id: 10).tick.should eq("super: called_tick")
end
end
6 changes: 3 additions & 3 deletions src/dataclass.cr
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ macro dataclass(class_def)
getter {{key.var}}
{% end %}

def initialize({% for key in literal %}
@{{key}},
{% end %})
def initialize({{*literal.map { |key| "@#{key}".id }}})
end

{{yield}}

def_equals_and_hash({% for key in literal %}@{{key.var}},{% end %})

def copy({% for key in literal %}{{key.var}} = @{{key.var}},{% end %}) : {{literal.type}}
Expand Down