0

Premise:

I want to separate the information necessary to instantiate a class from the information necessary to "run" the class. However, the information neccesary to "run" the class may differ from class to class. Thus, I imagine the class "has" specific information to run it, and the two go together.

Here is an example of my code:

trait Machine {
  type Params <: BaseParams

  def start(machineParams: Params): Unit
}

trait BaseParams {
  def speed: Int
  def power: Int
}

class FlyingMachine() extends Machine {
  type Params = FlyingParams

  override def start(machineParams: Params): Unit = {
    println(s"I'm flying with $machineParams")
  }
}

trait FlyingParams extends BaseParams {
  def height: Int
}


abstract class MachineOwner{

  val machine: Machine

  def params: machine.Params

  def startMachine(): Unit = {
    machine.start(params)
  }
}

This compiles, passes tests, I'm happy.

Problem: I'm using val machine: Machine in order to define def params: machine.Params. I've been told to make this a def to let the implementer have more freedom. If I do so, I can no longer refer to machine.Params

At this point, I'm at a loss for how to continue. I keep thinking that if this should be a def and definitely not a val, then my architecture is wrong.

So

  1. Is my approach to this problem wrong, given the premise I set out with?
  2. If it's not wrong, is there a way to still achieve this while using def instead of val in the MachineOwner class?

EDIT Given Alexey Romanov's answer, the last bit of the code would look like this

abstract class MachineOwner{

  type Params1 <: BaseParams

  def machine: Machine { type Params = Params1 }

  def params: Params1

  def startMachine(): Unit = {
    machine.start(params)
  }
}

class FlyingMachineOwner(
  machine: FlyingMachine
) extends MachineOwner {

  override type Params1 = FlyingParams

  override def params = FlyingParams(1,1,1)
}

But this doesn't compile because it expects an override specifically for def machine: Machine { type Params = Params1 }. How does one define that?

chr0nikler
  • 468
  • 4
  • 13
  • What does `Factory` actually produce? It seems like a tuple with two constants and a method that returns a `Unit`? – Andrey Tyukin Jul 04 '18 at 23:27
  • Ugh, I'll update the code. I didn't mean to name it `Factory` after the Factory builder pattern. It was just a small example. Imagine that it's job is just to manage `machine`. Start it, maybe stop if it an external event comes in, etc. – chr0nikler Jul 05 '18 at 00:26

1 Answers1

1

It really can't be answered without knowing desired semantics.

If MachineOwner is supposed to own a single machine, then "to make this a def to let the implementer have more freedom" is bad advice: the freedom it gives is exactly to return different machines from different calls to def machine and not to hold references to machines it gives out.

If it is supposed to have multiple machines, should all of them have the same Params type? Then you would do something like

abstract class MachineOwner{

  type Params1 <: BaseParams

  def machine: Machine { type Params = Params1 }

  def params: Params1

  def startMachine(): Unit = {
    machine.start(params)
  }
}

Or if not, then you need a different design again, maybe def params(machine: Machine): machine.Params. Etc. etc.

For the edit: you can do

class FlyingMachineOwner(
  _machine: FlyingMachine
) extends MachineOwner {

  override type Params1 = FlyingParams

  override def params = FlyingParams(1,1,1)

  override def machine = _machine
}

but it really seems unnecessarily complicated compared to what you get with type parameters.

Alexey Romanov
  • 167,066
  • 35
  • 309
  • 487
  • Thanks for the reply. Ideally, each `Machine` has its own `Params` type. To continue my example, there could be a `UnderwaterMachine` that has another parameter `SeaParams` that add things like oxgyen level, etc. My alternative solution is just to make `params` and optional field in `Machine`, and then just throw a `require` in my start before the code executes. But that forces a runtime violation versus a compile time violation, which rubs me slightly the wrong way. Hence me trying to leverage type safety. – chr0nikler Jul 05 '18 at 07:49
  • "each Machine has its own Params type" Yes, but that's different from the question "does each Machine returned by the same MachineOwner have the same Params type". – Alexey Romanov Jul 05 '18 at 07:52
  • Ohhh, I get it. Yes, each Machine returned by the same MachineOwner does have the same Params type". That question just helped clarify the difference. Let me fiddle with my code, and see what this changes for me. – chr0nikler Jul 05 '18 at 14:34
  • Edited my question with your suggestion. Am I missing how I should be overriding `machine`? This is my first run-in with type refinement. – chr0nikler Jul 05 '18 at 14:55
  • Because `Params1` is defined inside, `machine` has to be as well :( I'd suggest using type parameters instead of type members for both `Machine` and `MachineOwner`. – Alexey Romanov Jul 05 '18 at 17:03
  • Alrighty. I was afraid that would be the final answer. I'm torn between getting this to work on compile time and using the type members for design time vs instantiation time typing, but I'll opt for the former over the latter. – chr0nikler Jul 05 '18 at 17:18
  • I've added code which should work for this case to show it's possible. – Alexey Romanov Jul 05 '18 at 17:32
  • I'll mark your answer as correct @Alexey, because I believe you've demonstrated the best possible scenario with the parameters provided. Thanks for the effort. – chr0nikler Jul 06 '18 at 06:19