If you’ve read Scala Metaprogramming: A Glimpse into Eden, you should theoretically be able to implement Lombok.data.

So, I suggest you don’t read this article and try it yourself.

Define a Scala version of Lombok.data

@data
class A {
  var x: Int = _
  var y: String= _}Copy the code

We want the @data annotation to automatically generate the following code:

class A {
  var x: Int = _
  var y: String = _

  def getX() :Int = x
  def setX(paramX: Int) :Unit = { x = paramX }
  def getY() :Int = x
  def setY(paramY: String) :Unit = { y = paramY }
}
Copy the code

Why generate this code? Personally, I want to seamlessly use MyBatis in a project written in a mix of Spring Boot and Scala. When using Java, we can easily use Lombok.data to generate the getters and setters we need. In the Scala ecosystem, there is already a Case class, which is actually quite unorthodox for Pure Scala programmers.

When programming in Scala and Java, I think the most important thing is choice. There may be 100 ways to implement a feature, but the most appropriate way is always one.

I choose MyBatis, do not use the means of metaprogramming (in fact, I just learned this period of time), I do it like this:

import scala.beans.BeanProperty
class A {
  @BeanProperty var x: Int = _
  @BeanProperty var y: String= _}Copy the code

Using Vim column editing is actually fine. But my heart kept saying to myself: DO NOT REPEAT YOURSELF.

Reference implementation

/ /... annottees.map(_.tree).toList match { case q""" class $name { .. $vars } """ :: Nil => // Generate the Getter and Setter from VarDefs val beanMethods = vars.collect { case q"$mods var $name: $tpt = $expr" => val getName = TermName("get" + name.encodedName.toString.capitalize) val setName = TermName("set" + name.encodedName.toString.capitalize) println(getName) val ident = Ident(name) List ( q"def $getName: $tpt = $ident", q"def $setName(paramX: $tpt): Unit = { $ident = paramX }" ) }.flatten // Insert the generated Getter and Setter q""" class $name { .. $vars .. $beanMethods } """ case _ => throw new Exception("Macro Error") } } // ...Copy the code

Unit testing

The last metaprogramming article was really focused on building, so I posted the code for the build definition twice.

  test("generate setter and getter") {
    @data
    class A {
      var x: Int = _
      var y: String= _}val a = new A
    a.setX(12)
    assert(a.getX === 12)
    a.setY("Hello")
    assert(a.getY === "Hello")}Copy the code

Lombok has plugins in IntelliJ Idea to handle getters and setters automatically generated by applications that Idea cannot locate. If we only wanted MyBatis to recognize and use lombox.data, we wouldn’t have to customize a plugin for our Scala version. In our own code, there is no need to use getters and setters because Scala already supports them at the language level. (If you are confused, I recommend reading the sample chapters in Learn Scala And the Scala Practical Guide first.)

  test("handle operator in the name") {
    @data
    class B {
      var op_+ : Int= _}val b = new B
    b.setOp_+(42)
    assert(b.getOp_+ === 42)}Copy the code

This place also touches on a Scala-related point that I remember reading in Learn Scala Fast. In the reference implementation, the code associated with this single test is these two lines:

val getName = TermName("get" + name.encodedName.toString.capitalize)
val setName = TermName("set" + name.encodedName.toString.capitalize)
Copy the code

I’m not going to expand it here.

Unit testing style

I’ve always used ScalaTest for Scala projects, but the ScalaTest example on the ScalaTest website gives FlatSpec:

  "A Stack" should "pop values in last-in-first-out order" in {
    val stack = new Stack[Int]
    stack.push(1)
    stack.push(2)
    stack.pop() should be (2)
    stack.pop() should be (1)
  }
Copy the code

It’s probably this code style. We need to fill in some information in two places, which is kind of annoying. So I recommend the FunSuite style:

  test("A Stack pop values in last-in-first-out order"){
    val stack = new Stack[Int]
    stack.push(1)
    stack.push(2)
    stack.pop() should be (2)
    stack.pop() should be (1)
  }
Copy the code

I just need to fill in one sentence, without considering the subject, and be more IDE friendly.

Compile time vs. run time

These are two particularly important concepts in metaprogramming. Broadly speaking, these concepts are actually trying to remind us to pay attention to who (the process on that machine) is running our code.

When submitting the code, I forgot to clear the println(getName) used for debugging.

Using SBT to run our unit tests:

$ sbt
sbt:paradise-study> test[info] Compiling 1 Scalasource to $HOME/ a lot/paradise - study/lombok/target/scala 2.12 /test-classes ...
getX
getY
getOp_$plus[info] Done compiling. // Compiling is over. // Run [info] DataSuite: [info] - generate setter and getter [info] -handle operatorin the name
[info] Run completed in 437 milliseconds.
[info] Total number of tests run: 2
[info] Suites: completed 1, aborted 0
[info] Tests: succeeded 2, failed 0, canceled 0, ignored 0, pending 0
[info] All tests passed.
[success] Total time: 4 s, completed 2019-1-1 16:15:43
Copy the code

summary

This article is a Case Study of Scala metaprogramming. The complete project can be found at github.com/sadhen/para… .

Having just learned Scala metaprogramming in recent days, MY intuition tells me that Scala metaprogramming is not difficult, depending on whether the relevant Domain Knowledge has been stored in advance.

Scala metaprogramming: Implement Lombok.data