I have the following:
class Coordinate < T::Struct
const :x, Integer
const :y, Integer
end
class ThreeDCoordinate < T::Struct
const :x, Integer
const :y, Integer
const :z, Integer
end
What I want is to have my ThreeDCoordinate inherit x and y of Coordinate so I don't have to rewrite them in ThreeDCoordinate. How can I accomplish this?
There is a way to do this, using T::InexactStruct, but you will have to give up being able to strongly type the initializer of your structs:
# typed: true
class Coordinate < T::InexactStruct
const :x, Integer
const :y, Integer
end
class ThreeDCoordinate < Coordinate
const :z, Integer
end
coord = Coordinate.new(x: 2, y: 3)
T.reveal_type(coord.x)
T.reveal_type(coord.y)
threeD = ThreeDCoordinate.new(x: 2, y: 3, z: 4)
T.reveal_type(threeD.x)
T.reveal_type(threeD.y)
T.reveal_type(threeD.z)
# Note that the constructors are not typed anymore:
Coordinate.new(x: "foo", y: :bar) # This should fail but doesn't
Sorbet Playground Link
The problem with T::Struct and subclassing is that Sorbet creates an initializer for your struct that takes into account all of its declared fields. So for Coordinate the initializer has the signature params(x: Integer, y: Integer).void but for ThreeDCoordinate it has the signature params(x: Integer, y: Integer, z: Integer).void. Now these signatures are not compatible with each other, so Sorbet does not allow you to subclass one from the other.
T::InexactStruct allows you to give up the strong typing in the constructor and trade it in for being able to do inheritance on typed structs.
As far as I know, Sorbet does not support exactly the feature that you asked.
However, I am using interfaces to achieve similar typing goals:
module Coordinate
extend T::Helpers
extend T::Sig
interface!
sig { abstract.returns(Integer) }
def x; end
sig { abstract.returns(Integer) }
def y; end
end
module ThreeCoordinate
extend T::Helpers
extend T::Sig
include Coordinate
interface!
sig { abstract.returns(Integer) }
def z; end
end
class ThreeDLocation < T::Struct
extend T::Sig
include ThreeCoordinate
# Will be complained by Sorbet because of missing interface
const :x, Integer
end
class ThreeDCenter < T::Struct
extend T::Sig
include ThreeCoordinate
sig { override.returns(Integer) }
def x
0
end
sig { override.returns(Integer) }
def y
0
end
sig { override.returns(Integer) }
def z
0
end
end
Try out on Sorbet Playground to see how Sorbet static checks help ensure the typing.
Although there might be some repetitions in the codes, we have achieved the goal that classes implementing ThreeCoordinate is guaranteed to have the interface of Coordinate as well.
Also, using this approach, the implementation is more flexible. We can also implement something like ThreeDCenter above without being tied down to using Struct properties only.
Related
I set my file as typed: strict, and set my initialize method to take an array of floats, but srb tc reported that I had to use a T.let assertion in the body of the method:
# typed: strict
class Point
extend T::Sig
sig { params(c: T::Array[Float]).returns(t::Array[Float]) }
def initialize(c)
#c = c
end
end
Can’t Sorbet infer the type of #c from the signature?
Edit: As of 2019-12 this is no longer the case (see PR #2230). Now this code is perfectly valid (note that constructors' signatures declare void as the return type):
# typed: strict
class Point
extend T::Sig
sig { params(c: T::Array[Float]).void }
def initialize(c)
#c = c # Sorbet knows that c is a `T::Array[Float]`, so it assigns that type to #c
end
end
Previously:
This is a known limitation in Sorbet: "Why do I need to repeat types from the constructor?"
TL;DR:
[Sorbet] cannot reuse static type knowledge in order to automatically determine the type of an instance or class variable.
It was also reported in #1666 Seemingly unnecessary type annotation of instance variables
I read that it is not possible to have several constructors for a class. So the following code won't work:
class C
def initialize x
initialize x,0
end
# Some methods using the private constructor…
def foo
# …
bar = C.new 3,4
# …
end
private
def initialize x,y
#x = x
#y = y
end
end
I have thought about replacing the public constructor by a static method, but that would prevent other classes to extend C. I also have thought about making a private post-initializing method:
class C
def initialize x
post_init x,0
end
# Some methods using the private constructor…
def foo
# …
bar = C.new baz
bar.post_init 3,4
# …
end
private
def post_init x,y
#x = x
#y = y
end
end
But here, the post_init is called twice, which is not a good thing.
Is there a way to give a public constructor, while having in private a more complete way to make a new instance? If not, what is the best way to do something similar?
I guess this would do what you expect.
class C
def initialize(x, y = 0)
#x = x
#y = y
end
def self.with_position
new(3, 4)
end
end
c1 = C.new(5)
c2 = C.with_position
If you want to prohibit setting y by anyone from outside the class, you can use some private method behind the scenes (as you suggested) and konstructor gem
class C
def initialize(x)
set_coords(x, 0)
end
konstructor
def with_position
set_coords(3, 4)
end
private
def set_coords(x, y)
#x = x
#y = y
end
end
c1 = C.new(5)
c2 = C.with_position
A simple way is to accept options for initialize, you can have an if statement in there that covers the private or public cases.
Ruby doesn't really have this concept of 'private class' in a simple way such as saying 'private'.
You can see How to I make private class constants in Ruby for a way to make a private constant (since classes are constants). You'd make a class method that returns an anonymous class (Class.new do ... end). Then mark that class method private with private_class_method.
A better solution would be to make two classes with different initializes. Common functionality could be in a separate class or module. If it's a class, then the way to include them in the 'public/private' classes would be inheritance. If it's a module, then you'd include/extend.
I'm working on a Grid class that inherits from Matrix:
class Grid < Matrix
def self.[](x,y=x)
if x.is_a? String
ary = x.lines.map { |l| l.strip.split('|') }.map.with_index do |col,x|
col.map.with_index { |cell,y| ::Cell.new x: x, y: y, alive: !!(cell =~ /O/i) }
end
super *ary
else
super *Array.new(y) { [::Cell.new(x: x, y: y, alive: [true,false].sample)] * x }
end
end
end
I can't seem to be able to overwrite ::initialize since it's private. The above works but yields instances of Matrix class instead of my custom class, so I'm stuck. Realized my class wasn't instantiating when calling to_s and receiving "Matrix[[X,O],[O,O]]" stuff. What I am missing?
There is no ::new method in ruby, you define it via #initialize
When you call super it's calling Matrix::[] with the arguments provided.
Look at the source code:
def Matrix.[](*rows)
Matrix.rows(rows, false)
end
You can try defining Grid::rows with your logic instead. Or just override #initialize
Incidentally this is poorly written, they should have done rows(rows,false) (without the Matrix) to prevent this very issue.
I would like to change the self value of a float instance.
I have the following method :
class Float
def round_by(precision)
(self * 10 ** precision).round.to_f / 10 ** precision
end
end
And I would like to add the round_by! method which will modify the self value.
class Float
def round_by!(precision)
self = self.round_by(precision)
end
end
But I got an error saying I can't change the value of self.
Any idea ?
You can't change the value of self. It always points to the current object, you can't make it point to something else.
When you want to mutate the value of an object, you either do this by calling other mutating methods or setting or changing the values of instance variables, not by trying to reassign self. However in this case, that won't help you, because Float doesn't have any mutating methods, and setting instance variables won't buy you anything, because none of the default float operations are affected by any instance variables.
So the bottom line is: you can't write mutating methods on floats, at least not in the way you want.
You can also create a class and store the float in a instance variable:
class Variable
def initialize value = nil
#value = value
end
attr_accessor :value
def method_missing *args, &blk
#value.send(*args, &blk)
end
def to_s
#value.to_s
end
def round_by(precision)
(#value * 10 ** precision).round.to_f / 10 ** precision
end
def round_by!(precision)
#value = round_by precision
end
end
a = Variable.new 3.141592653
puts a #=> 3.141592653
a.round_by! 4
puts a #=> 3.1416
More about using "class Variable" here.
This is actually a really good question and I'm sorry to say that you can't - at least not with the Float class. It's immutable. My suggestion would be to create your own class the implements Float (aka inherits all the methods), like so in pseudo-code
class MyFloat < Float
static CURRENT_FLOAT
def do_something
CURRENT_FLOAT = (a new float with modifications)
end
end
I have this code:
def setVelocity (x, y, yaw)
setVelocity (Command2d.new(x,y,yaw))
end
def setVelocity (vel)
......
end
vel is a Command2D class that has 3 attributes, is Comparable and defines + , basically is a convenient class for me to manage those 3 attributes, so I want to use it internally in my library (dont want to make them private, either give them weird names).
But Ruby seems to keep only the last setVelocity even when the number of parameters is different. so when I call setVelocity with 3 parameters will say that I need to call that method with only one parameter.
Ruby doesn't really support overloading.
This page gives more details and a workaround. Basically you create a single method with a variable number of parameters, and deal with them appropriately.
(I'd personally recommend writing one method to recognise the two different "faked overloads" and then one method for each overload, with different names reflecting the different parameters.)
Alternatively, just provide different names to start with :)
Just for comparison, here's how I would solve it:
#!/usr/bin/env ruby
class Command2D
def initialize(x, y, yaw)
#command = [x, y, yaw]
end
end
class Vehicle
def velocity=(command_or_array)
case command_or_array
when Command2D
self.velocity_from_command = command_or_array
when Array
self.velocity_from_array = command_or_array
else
raise TypeError, 'Velocity can only be a Command2D or an Array of [x, y, yaw]'
end
end
private
def velocity_from_command=(command)
#velocity = command
end
def velocity_from_array=(ary)
raise TypeError, 'Velocity must be an Array of [x, y, yaw]' unless ary.length == 3
#velocity = Command2D.new(*ary)
end
end
v1 = Vehicle.new
v1.velocity = Command2D.new(1, 2, 3)
v2 = Vehicle.new
v2.velocity = [1, 2, 3]
p v1
p v2
Use attr_accessor to add attributes and you will get getters and setters automatically.
Alternatively use attr_reader or attr_writer to get read-only or write-only attributes.
class Foo
attr_accessor :velocity
end
You can now set and get the value of this attribute like this:
foo = Foo.new
foo.velocity = 100
puts foo.velocity # => 100
If you want to add methods to set the attribute based on some parameters, use a name reflecting what kind of input are expected:
def velocity_from_yaw(x, y, yaw)
velocity = Command2d.new(x, y, yaw)
end
You can probably find a much better name in this case, but I don't know what your x, y and yaw really mean in your context.