Reputation: 4470
I'm writing a plugin for SpongeAPI in Scala.
There are several "constants" that can not be initialized until a certain lifecycle event of the plugin is fired, because the system isn't ready yet.
I was thinking that I would like to encode this requirement as a trait, however Scala doesn't have trait parameters, nor do I really need to keep the event around after the trait has been constructed (local parameter) just need proof that it existed at time of construction.
I've looked on stackoverflow to find suggestions of val's and def's that take the field implicitly, but that also means capturing the event for the entire lifecycle of the class, potentially causing a leak as it may reference other classes that need to be garbage collected.
Any suggestions?
Edit: my imaginary syntax would be something like the following.
//Imaginary
class LateConstants extends ImaginaryTrait[GameStartingServerEvent] {
//this class is now impossible to instantiate without a GameStartingServerEvent
//Possibly using a factory method or something in a companion class?
val wool = BlockTypes.WOOL
}
But I realize that this likely isn't possible on it's own. So I was exploring possible solutions:
package au.id.rleach.common
import org.spongepowered.api.block.BlockTypes
import org.spongepowered.api.command.CommandManager
import org.spongepowered.api.command.spec.CommandSpec
import org.spongepowered.api.event.game.state.GameStartingServerEvent
import org.spongepowered.api.text.Text
trait CommandRegistration {
protected[this] implicit def e: GameStartingServerEvent
}
class constants(val e: GameStartingServerEvent) extends CommandRegistration {
//Good, but now contains a refernece to e forever.
}
trait CommandRegistration2 {
def registerCommands(gameStartingServerEvent: GameStartingServerEvent)
}
class constants2 extends CommandRegistration2{
override def registerCommands(gameStartingServerEvent: GameStartingServerEvent): Unit = {
//Better, but now the class has to run a method after construction in order to initialize correctly,
//seems side effecty.
}
}
class constants3(implicit gameStartingServerEvent: GameStartingServerEvent){
//Even better, constructor doesn't need the parameter passed explicitly, can only be instantiated in the right context.
//No trait, can't share this functionality across classes :( What about orthgonality?
}
class constants4 private (commandManager: CommandManager, basePlugin: BasePlugin){
val exampleSpec = CommandSpec.builder().description(Text.of("example")).build()
val exampleMapping = commandManager.register(basePlugin, exampleSpec, "example")
}
trait CommandRegistration4 {
def registerCommands(commandManager: CommandManager)(implicit gameStartingServerEvent: GameStartingServerEvent, plugin: BasePlugin)
}
object constants4 extends CommandRegistration4{
def registerCommands(commandManager: CommandManager)(implicit gameStartingServerEvent: GameStartingServerEvent, plugin: BasePlugin): constants4 = {
new constants4(commandManager, plugin)
}
}
trait LifecycleDependency[EVENT, RETURN] {
def initialize(implicit event: EVENT, plugin: BasePlugin):RETURN
}
trait CommandRegistration5 extends LifecycleDependency[GameStartingServerEvent, constants5]
object constants5 extends CommandRegistration5 {
override def initialize(implicit event: GameStartingServerEvent, plugin: BasePlugin): constants5 = {
new constants5(plugin)
}
}
class constants5 private (plugin: BasePlugin){
val wool = BlockTypes.WOOL //only valid after GameStartingServerEvent
val obsidian = BlockTypes.OBSIDIAN
} //No nice easy way to inherit the constraints of the trait, requires all the bolierplate above.
class Constants6{
val wool = BlockTypes.WOOL
}
object Constants6{
def apply()(implicit gameStartingServerEvent: GameStartingServerEvent)={
new Constants6
}
}
object test{
def test()={
Constants6()//Not bad, but requires the above in every implementation.
}
}
Edit: An example plugin for SpongeAPI to make it more clear where and how the code is being called, SpongeAPI is outside of my dev control so the events can not be changed.
@Plugin(id = "au.id.rleach.memorystones", name = "MemoryStones", version = "0.0.1", dependencies = "required-after:au.id.rleach.multiblocks@[0.0.1]")
class ExamplePlugin {
//ExamplePlugin gets instantiated external to my plugin using Guice from a primarily Java environment.
@Listener
def onGameReadyForListeners(event: GameStartingServerEvent) = {
/**Annotated method that gets called with the event when the game is
ready and the constants can be accessed safely. I could just access
everything I need from this point onwards, however I was hoping to
encode it in the type system so I was unable to make the error of
referencing anything too soon. It's also possible to register other
classes to listen for events by listening for this event (or an
earlier preinitialization event and registering that class instance
with the API.
**/
val late: LateConstants = new LateConstants(event)
val somethingThatReliesOnLateConstants = new Something(late)
}
}
Upvotes: 1
Views: 107
Reputation: 16422
This example requires game event to be created per game (not globally - see below):
scala> trait GameStartingEvent {
| println("GAME STARTED EVENT!")
| }
defined trait GameStartingEvent
scala> trait AfterGameStarted {
| val collectEvidence = gameStartedEvidence
|
| println("START OF THE POST GAME")
|
| object gameStartedEvidence extends GameStartingEvent
|
| println("END OF THE POST GAME")
| }
defined trait AfterGameStarted
scala> val after = new AfterGameStarted {}
GAME STARTED EVENT!
START OF THE POST GAME
END OF THE POST GAME
after: AfterGameStarted = $anon$1@6493d8f
scala> trait GameOne extends AfterGameStarted
defined trait GameOne
scala> val game1 = new GameOne {}
GAME STARTED EVENT!
START OF THE POST GAME
END OF THE POST GAME
game1: GameOne = $anon$1@3e26dada
scala> trait GameTwo
defined trait GameTwo
scala> val game2 = new GameTwo with AfterGameStarted {}
GAME STARTED EVENT!
START OF THE POST GAME
END OF THE POST GAME
game2: GameTwo with AfterGameStarted = $anon$1@136c834
scala> val game3 = new AfterGameStarted with GameTwo {}
GAME STARTED EVENT!
START OF THE POST GAME
END OF THE POST GAME
game3: AfterGameStarted with GameTwo = $anon$1@404043cb
Scala is lazy in some ways, so here when we create an object in trait AfterGameStarted
and init the trait the object still does not get created. We have to reference it explicitly with val collectEvidence ...
in the trait constructor.
Later example shows that this works with inheritance and mixins without having to repeat the boilerplate code.
I still think that this is not the best approach to the problem and it might break if you are not careful under some circumstances.
If you need single global event to be initialized then it's even easier:
scala> object GlobalGameStartingEvent {
| println("GAME STARTED EVENT!")
| }
defined object GlobalGameStartingEvent
scala> trait AfterGameStarted {
| val ensureGameStarted = GlobalGameStartingEvent
|
| println("START/END OF THE POST GAME")
| }
defined trait AfterGameStarted
scala> val after = new AfterGameStarted {}
GAME STARTED EVENT!
START/END OF THE POST GAME
after: AfterGameStarted = $anon$1@65058509
scala> val after = new AfterGameStarted {}
START/END OF THE POST GAME
Worth mentioning DelayedInit
: http://www.scala-lang.org/files/archive/nightly/docs/library/index.html#scala.DelayedInit, which was deprecated:
trait GameStartingEvent extends DelayedInit {
override def delayedInit(body: => Unit) = {
println("GAME STARTED EVENT!")
body
}
}
class AfterGameStarted extends GameStartingEvent {
println("START/END OF THE POST GAME")
}
after: AfterGameStarted = $anon$1@1e8516be
scala> val after = new AfterGameStarted {}
GAME STARTED EVENT!
START/END OF THE POST GAME
after: AfterGameStarted = $anon$1@3838ad7
From the docs:
Classes and objects (but note, not traits) inheriting the DelayedInit marker trait will have their initialization code rewritten as follows: code becomes delayedInit(code).
Upvotes: 2