一些 Scala 的 Tips



一些良好的实践

工具相关

Ammonite

自定义 Ammonite 国内仓库镜像,修改 $HOME/.ammonite/predef.sc ,添加如下内容:


import coursierapi.MavenRepository

interp.repositories() ++= Seq(
  MavenRepository.of("file://" + scala.sys.env("HOME") + "/.m2/repository/"),
  MavenRepository.of("file://" + scala.sys.env("HOME") + "/.ivy2/cache/"),
  MavenRepository.of("https://maven.aliyun.com/repository/public/"),
  MavenRepository.of("https://maven.aliyun.com/repository/central/"),
  MavenRepository.of("https://maven.aliyun.com/repository/google/"),
  MavenRepository.of("https://maven.aliyun.com/repository/apache-snapshots/"),
  MavenRepository.of("https://mirrors.huaweicloud.com/repository/maven/"),
  MavenRepository.of("https://repo1.maven.org/maven2/")
)

如果是在 Windows 下,环境变量 HOME 改为 HOMEPATH 。

查看编译器对表达式/语法糖的翻译结果,可以用 universe.reify 方法:


// reify方法用于对一段表达式参数生成语法树。
import reflect.runtime.universe._

reify( for ( i <- 1 to 10 ) println(i) )

Mill

在使用 Mill 的 assembly 打包的时候,有个深坑需要注意:mill 默认会在 jar 包里加些脚本内容,以使打包后的 jar 包可以直接执行。但是这种方式处理后的 jar 包格式可能会使某些系统无法识别(用 jar tvf 可能会报错)。解决的办法是在 build.sc 中添加如下内容:


override def prependShellScript: T[String] = ""

临时的数据库服务器

有的时候,会需要一个临时的数据库服务器来进行一些测试。此时,不必在本地完整安装全套数据库的服务器组件,可以在内存里模拟一个临时的数据库服务器实例。

以下方案与 Testcontainers 不同,不需要本地机器上预写安装 Docker 环境。

以在 Ammonite 中执行为例:

运行一个 MySQL 服务器:


{
  import $ivy.{
    `com.wix:wix-embedded-mysql:4.6.1`
  }
  import java.util.concurrent.{ TimeoutException, TimeUnit }
  import com.wix.mysql.config.MysqldConfig.aMysqldConfig
  import com.wix.mysql.config.SchemaConfig.aSchemaConfig
  import com.wix.mysql.config.Charset.UTF8
  import com.wix.mysql.EmbeddedMysql.anEmbeddedMysql
  import com.wix.mysql.distribution.Version.v5_7_latest

  val config = aMysqldConfig(v5_7_latest).
               withCharset(UTF8).
               withPort(3306).
               withUser("SomeUserName", "SomePassword").
               withTimeZone("Asia/Shanghai").
               withTimeout(30, TimeUnit.MINUTES).
               withServerVariable("max_connect_errors", 100).build()

  val server = anEmbeddedMysql(config).
               addSchema(aSchemaConfig("test").build()).
               start()
}

// 关闭服务器: server.stop

运行一个 PostgreSQL 服务器:


{
  import $ivy.{
    `com.opentable.components:otj-pg-embedded:0.13.3`
  }
  import com.opentable.db.postgres.embedded.EmbeddedPostgres

  val server = EmbeddedPostgres.builder().setPort(5432).start()
}

// 关闭服务器: server.close

代码片段

线程池

Future


// 使用 Java 的 ExecutorService
import java.util.concurrent.{Executors, ExecutorService}
// 使用 Monix 的 ExecutorService
import $ivy.{`io.monix::monix:3.2.2`}
import monix.execution.Scheduler

import scala.concurrent.{Future, ExecutionContext}


// 使用 Java 的 ExecutorService
// 偷懒 import scala.concurrent.ExecutionContext.Implicits.global

implicit val ec = ExecutionContext.fromExecutor(
  Executors.newFixedThreadPool(6)]
)
// 使用 Monix 的 ExecutorService
// 偷懒 import monix.execution.Scheduler.Implicits.global
implicit val ec = Scheduler.fixedPool(name="testpool", poolSize=6)

val sumFuture: Future[Int] = Future[Int] {
  var sum = 0
  for(i <- Range(1,100000)) sum = sum + i
  sum
}

// 同步阻塞
import scala.concurrent.Await
import scala.concurrent.duration.Duration

val rs: Int = Await.result(sumFuture, Duration.Inf)


// 回调
import scala.util.{Try, Success, Failure}

//// 仅在成功后调用,使用 onSuccess
//// 或 foreach(A => Unit)、map(A => A)、flatMap(A => Future[A])

sumFuture.onSuccess {
  case number => println(s"Succeed with: ${number}")
}


//// 成功失败都有调用的,使用 onComplete(Try[A] => Unit)、andThen
//// 或 transform(Try[A] => Try[B])、transformWith(Try[A] => Future[B])

def printResult[A](result: Try[A]): Unit = result match {
  case Failure(exception) => println(s"Failed with: ${exception.getMessage}")
  case Success(number)    => println(s"Succeed with: ${number}")
}
sumFuture.onComplete(printResult)

sumFuture.andThen {
  case Success(v) => println(s"The answer is $v")
} andThen {
  case Success(_) =>  sendSuccessSignalHTTPRequest()
  case Failure(_) =>  sendFailureSignalHTTPRequest()
}

val transformed = Future.successful(42).transform {
  case Success(value) => Success(s"Successfully computed the $value")
  case Failure(cause) => Failure(new IllegalStateException(cause))
}

val transformed2 = Future.successful(42).transformWith {
  case Success(value) => Future.success(s"Successfully computed the $value")
  case Failure(cause) => Future.failure(new IllegalStateException(cause))
}


// 对特定异常进行“掩盖”
val recoveredF: Future[Int] = Future(3 / 0).recover {
  case _: ArithmeticException => 0
}

val recoveredWithF: Future[Int] = Future(3 / 0).recoverWith {
  case _: ArithmeticException => sumFuture
}

val failedInt: Future[Int] = Future.failed(
  new IllegalArgumentException("Boom!")
)
failedInt.fallbackTo(Future.successful(42))

Task


// 使用 Monix 的 ExecutorService
import $ivy.{`io.monix::monix:3.2.2`}
import monix.execution.Scheduler
import monix.execution.Scheduler.Implicits.global
import monix.eval.Task
import monix.execution.CancelableFuture

// 这里只是定义了运算,并未开始执行。
val sumTask: Task[Int] = Task {
  var sum = 0
  for(i <- Range(1,100000)) sum = sum + i
  sum
}

import scala.util.{Try, Success, Failure}

// 开始执行运算
val cancelable1 = sumTask.runOnComplete {
  case Success(value) => println(value)
  case Failure(ex) => println(s"ERR: ${ex.getMessage}")
}

cancelable1.cancel()

val cancelable2 = sumTask.runAsync {
  case Right(value) => println(value)
  case Left(ex) => println(s"ERR: ${ex.getMessage}")
}


// Task 转换为 Future
val future1 = sumTask.runToFuture

// Future 转换为 Task
val task1 = Task.deferFuture {
  Future { println("Do something.") }
}


// 立即执行
val sumTask: Task[Int] = Task.now {
  var sum = 0
  for(i <- Range(1,100000)) sum = sum + i
  sum
}

日期时间

Scala 使用的是 Java 的日期时间 API 。自 Java 8 后提供了 java.time、java.time.format、java.time.chrono、java.time.zone 和 java.time.temporal 包。


// 时区
import java.time.ZoneId
val tzDefault = ZoneId.systemDefault
val tzHK = ZoneId.of("Asia/Hong_Kong")

import java.time.ZoneOffset
val tzBJ = ZoneOffset.of("+8")


// 时间戳是时间线上的一个点。在纪元 1970-01-01T00:00:00Z 后是正值,之前是负值。
import java.time.Instant

val timeStamp0 = Instant.EPOCH
val timeStampA = Instant.now()
timeStampA.getEpochSecond  // 精确到秒
timeStampA.toEpochMilli    // 精确到毫秒

// 日期时间转时间戳
val timeStampB = LocalDateTime.now().toInstant(ZoneOffset.of("+8"))
// 带时区的日期时间转时间戳不带参数
val timeStampC = ZonedDateTime.now(ZoneId.of("US/Pacific")).toInstant   


// LocalTime 只有时间、LocalDate 只有日期、LocalDateTime 既有日期又有时间
import java.time.LocalDate
import java.time.LocalTime
import java.time.LocalDateTime
import java.time.ZonedDateTime
import java.time.temporal.ChronoUnit

val date1 = LocalDate.now()
val date2 = LocalDate.of(2020, 3, 2)
val tomorrow = date1.plusDays(1)
val yestoday = date1.minus(1, ChronoUnit.DAYS)

val time1 = LocalTime.now()

val dateTime1 = LocalDateTime.now()
val dateTime2 = LocalDateTime.of(2020, 3, 2, 17, 5, 1)
val dateTime3 = LocalDateTime.ofInstant(  // 时间戳转日期
  Instant.now(),
  ZoneId.of("Asia/Shanghai")
)
val dateTime4 = ZonedDateTime.now(ZoneId.of("US/Pacific"))


// 时间间隔 Period 基于日期, Duration 基于时间
import java.time.Period

val period1 = Period.of(3, 2, 1)  // 3年两个月一天的间隔
val period2 = Period.ofWeeks(2)
val period3 = Period.between(LocalDate.now(), LocalDate().minusDays(5))

import java.time.Duration

val duration1 = Duration.of(4, ChronoUnit.HOURS) // 4个小时的间隔
LocalDateTime.now().minus(duration1)
val duration2 = Duration.between(
  LocalDateTime.now(),
  LocalDateTime.now().minusHours(5)
)


// 相较于 java.text.SimpleDateFormat ,
// java.time.format.DateTimeFormatter 是线程安全的。
import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle

val fmt1 = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)
val fmt2 = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
datetime1.format(fmt1)

import java.time.format.DateTimeParseException

// 解析日期/时间字符串
val dateTime5 : LocalDateTime = LocalDateTime.parse("2020-01-02 13:01:06", fmt1)

集合


// 类似于 Clojure 的 (partition-by identity some-sequence)
@tailrec
def splitOnDifferent[A](s: List[A], acc: Seq[Seq[A]] = Seq()): Seq[Seq[A]] = {
  s match {
    case Nil => acc
    case fst :: rest =>
      val (part1, part2) = s.span( _ == fst )
      splitOnDifferent(part2, acc :+ part1)
  }
}


// 模式匹配中处理类型擦除
import scala.reflect.runtime.universe.TypeTag

def checkType[T: TypeTag](v: T) = typeOf[T] match {
  case t if t =:= typeOf[List[String]] => "List of Strings"
  case t if t =:= typeOf[List[Int]] => "List of Ints"
  case _ => "Any other type"
}

系统环境变量


// 用 Scala 的 sys 对象
sys.env.get("PWD")

// 用 Scala 的 Properties 对象
scala.util.Properties.envOrElse("PWD", "undefined")

scala.util.Properties.envOrNone("PWD")

// 用 Java 的 System 对象
System.getenv("PWD")

密码学相关


import $ivy.`org.scorexfoundation::scrypto:2.1.10`

// Base58
import scorex.util.encode.Base58
val decoded = Base58.decode("b2rhe5P4gXftAwvA4eXQ5HJwsER2owDyS9sKaQRRVQPn93bA")
decoded match {
  case Success(v) => v.map("%02x" format _).mkString
  case Failure(f) => println(f)
}

本文链接: https://paxinla.github.io/posts/2020/04/yi-xie-scala-de-tips.html

知识共享许可协议 本作品采用知识共享署名-非商业性使用-相同方式共享 3.0 中国大陆许可协议进行许可,欢迎转载、演绎,
但是必须保留本文的署名 Charles(包含链接),且不得用于商业目的。