콘텐츠로 이동

2023 02 01

2023-02-01

Scala 기초 투어

추출 오브젝트 => 뭔소리지...?

  • 개요
    • unapply 메서드가 있는 object
  • 예시
    object Twice {
      def apply(x: Int): Int = x * 2
      def unapply(z: Int): Option[Int] = if (z%2 == 0) Some(z/2) else None
    }
    
    object TwiceTest extends App {
      val x = Twice(21)
      x match { case Twice(n) => Console.println(n) }
    }
    
    object CustomerID {
      def apply(name: String) = s"$name--${Random.nextLong()}"
    
      def unapply(customerID: String): Option[String] = {
        val stringArray: Array[String] = customerID.split("--")
        if (stringArray.tail.nonEmpty) Some(stringArray.head) else None
      }
    }
    
    val customer1ID = CustomerID("Sukyoung") // Sukyoung-23239492918
    customer1ID match {
      case CustomerID(name) => println(name) // prints Sukyoung
      case _ => println("Couldn't extract")
    }
    

For문

  • 개요
    • For문 다루는 방법 제공
    • 약간 stream과 느낌이 비슷
  • 예시

    • for안의 if로 필터링
    • yield로 매핑
      case class User(name: String, age: Int)
      
      val userBase = List(
        User("Travis", 28),
        User("Kelly", 33),
        User("Jenny", 44),
        User("Dennis", 23)
      )
      
      // List[String]
      val twentySomethings =
        for (user <- userBase if user.age >= 20 && user.age < 30)
        yield user.name // add this to a list, map function과 비슷
      
      twentySomethings.foreach(println) // Travis, Dennis
      
    • 중첩 포문 + generator 합쳐서 쇼부
      def foo(n: Int, v: Int) =
        for (i <- 0 until n;
             j <- 0 until n if i + j == v)
        yield (i, j)
      
      foo(10, 10) foreach {
        case (i, j) => 
          println(s"($i, $j)") // (1, 9), (2, 8)... (9, 1)
      }
      
    • yield로 매핑 안하면 Unit으로 반환
      def foo(n: Int, v: Int) =
        for (i <- 0 until n;
             j <- 0 until n if i + j == v)
        println(s"($i, $j)")
      
      foo(10, 10)
      

제네릭 클래스

  • 개요
    • Java의 제네릭과 비슷
    • 강타입언어에서 필요
    • A를 주로 표시자로 씀
  • 예시

    • 자바랑 비슷하지?
      class Stack[A] {
        private var elements: List[A] = Nil // Nil == Empty List == List[Nothing]
        def push(x: A): Unit =
          elements = x :: elements
        def peek: A = elements.head
        def pop(): A = {
          val currentTop = peek
          elements = elements.tail
          currentTop
        }
      }
      
    • 서브 타입 쌉가능
      class Fruit
      class Apple extends Fruit
      class Banana extends Fruit
      
      val stack = new Stack[Fruit]
      val apple = new Apple
      val banana = new Banana
      
      stack.push(apple)
      stack.push(banana)
      
    • invariant (공뭐시꺵이.. 자바에도 있는 개념이지?)
      • Stack[A]와 Stack[B]는 A==B 일때만 하위로 사용가능

Variance

  • 개요
    • 서브 타이핑 관계
    • 해당 Variance 중 선택 가능: covariant, contravariant, invariant (어노테이션 없음)
    • 자바보다 훨씬 유연한 제네릭 Variance를 제공
        class Foo[+A] // covariant 공변
        class Bar[-A] // contravariant
        class Baz[A] // invariant
        ```
      
      - 예시
      ```scala
      abstract class Animal {
        def name: String
      }
      
      case class Cat(name: String) extends Animal
      case class Dog(name: String) extends Animal
      
  • Invariance
    • 타입 파라미터 기본적으로 스칼라에서 Invariant 함
      class Box[A](var content: A)
      
      
      val myCatBox: Box[Cat] = new Box[Cat](Cat("Felix"))
      val myAnimalBox: Box[Animal] = myCatBox // 컴파일 안 됨 Cat Box -> Animal로 변경을 할 수 있어도 Cat Box 안에 Dog를 넣을 수 있는 가능성을 제공하게 됨
      val myAnimal: Animal = myAnimalBox.content
      
  • Covariance

    • type[+A]로 정의가 된다면...
      • type[부모] <- type[자식] 이 성립이 됨
      • 다만 출력에 대해서만 적용
        class ImmutableBox[+A](val content: A)
        val catBox: ImmutableBox[Cat] = new ImmutableBox[Cat](Cat("Felix"))
        val animalBox: ImmutableBox[Animal] = catBox
        
    • 대표적으로 List가 Covariance
      def printAnimalNames(animals: List[Animal]): Unit =
        animals.foreach {
          animal => println(animal.name)
        }
      
      val cats: List[Cat] = List(Cat("Whiskers"), Cat("Tom"))
      val dogs: List[Dog] = List(Dog("Fido"), Dog("Rex"))
      
      // prints: Whiskers, Tom
      printAnimalNames(cats)
      
      // prints: Fido, Rex
      printAnimalNames(dogs)
      
  • Contravariance
    • 이번엔 출력이 아니라 집어 넣는거에 초점을 맞춰보자
    • type[자식] <- type[부모] 가 가능해져
    • 그러면 부모에서 투상화시켜 처리할 수 있는 로직을 자식에게 부여할 수 있음
      abstract class Serializer[-A] {
        def serialize(a: A): String
      }
      
      val animalSerializer: Serializer[Animal] = new Serializer[Animal] {
        override def serialize(animal: Animal): String = s"""{ "name": "${animal.name}" }"""
      }
      
      val catSerializer: Serializer[Cat] = animalSerializer
      catSerializer.serialize(cat("Felix"))
      

Upper Type Bounds & Lower Type Bounds

  • Upper Type Bounds
    • 자바 제네릭 extends 와 비슷
      abstract class Animal {
        def name: string
      }
      
      abstract class Pet extends Animal {}
      
      class Cat extends Pet {
        override def name: String = "Cat"
      }
      
      class Dog extends Pet {
        override def name: String = "Dog"
      }
      
      class Lion extends Animal {
        override def name: String = "Lion"
      }
      
      class PetContainer[P <: Pet](p: P) {
        def pet: P = p
      }
      
      val dogContainer = new PetContainer[Dog](new Dog)
      val catContainer = new PetContainer[Cat](new Cat)
      val lionContainer = new PetContainer[Lion](new Lion) // 이딴거 안대! Animal 상속이여
      
  • Lower Type Bounds
    • 자바 제네릭 super 와 비슷
      trait Bird
      case class AfricanSwallow() extends Bird
      case class EuropeanSwallow() extends Bird
      
      val africanSwallow: List[AfricanSwallow] = Nil.prepend(AfricanSwallow())
      val swallowsFromAntarctica: List[Bird] = Nil
      val someBird: Bird = EuropeanSwallow()
      
      val birds: List[Bird] = africanSwallows
      val someBirds = africanSwallow.prepend(someBird)
      val moreBirds = birds.prepend(EuropeanSwallow())
      val allBirds = africanSwallow.prepend(EuropeanSwallow())
      val error = moreBirds.prepend(swallowsFromAntarctica)
      

내부 클래스

  • 개요
    • 독특한 건 내부 클래스가 외부 클래스에 따라 별도의 타입으로 정의됨
    • 인스턴스에 한정된 개념
      • 자바처럼 쓰려면 외부클래스#내부클래스 처럼 타입 정의하면 가능
  • 예시
    class Graph {
      class Node {
        var connectedNodes: List[Node] = nNil
        def connectTo(node: Node): Unit = {
          if (!connectedNodes.exists(node.equals)) {
            connectedNodes = node :: connectedNodes
          }
        }
      }
    
      var nodes: List[Node] = Nil
      def newNode: Node = {
        val res = new Node
        nodes = res :: nodes
        res
      }
    }
    
    val graph1: Graph = new Graph
    val node1: graph1.Node = graph1.newNode
    val node2: graph1.Node = graph1.newNode
    val node3: graph1.Node = graph1.newNode
    node1.connectTo(node2)
    node3.connectTo(node1)
    
    val graph2: Graph = new Graph
    val node3: graph2.Node = graph2.newNode
    node1.connectTo(node3) // 안대!!!!
    

추상 타입 멤버

trait Buffer {
  type T
  val element: T
}

abstract class SeqBuffer extends Buffer {
  type U
  type T <: Seq[U]
  def length = element.length
}

abstract class IntSeqBuffer extends SeqBuffer {
  type U = Int
}

def newIntSeqBuf(elem1: Int, elem2: Int): IntSeqBuffer =
  new IntSeqBuffer {
    type T = List[U]
    val element = List(elem1, elem2)
  }

val buf = newIntSeqBuf(7, 8)
println("length = " + buf.length)
println("content = " + buf.element)

합성 타입

  • 객체의 타입을 여러 다른 타입의 서브 타입으로 표현해야 하는 경우
  • 합성 타입으로 이를 표현할 수 있음 (객체 타입들의 교차점) with
  • Cloneable, Resetable
    trait Cloneable extends java.lang.Cloneable {
      override def clone(): Cloneable = {
        super.clone().asInstanceOf[Cloneable]
      }
    }
    
    trait Resetable {
      def reset: Unit
    }
    
    // 클론하고 리셋할 수 있는 객체를 넘겨 받고 싶다면 이런식으로...
    def cloneAndReset(obj: Cloneable with Resetable): Cloneable = {
      val cloned = obj.clone()
      obj.reset
      cloned
    }
    

셀프 타입

  • 어렵네...
  • 어떤 trait를 확장해서 쓰려할 때 다른 trait도 가져와서 사용해야해를 강제하는 것

묵시적 파라미터

  • 개요
    • 메서드에 파라미터 전달해 호출할 때,
    • 반복 전달하는 파라미터를 자동으로 전달
    • 타입에 따라 다양한 파라미터를 자동으로 전달
    • implicit 키워드를 기반으로 생략되어있는 파라미터를 뚝딱 찾아서 전달 할 수 있음
  • 예시
    abstract class Monoid[A] {
      def add(x:A, y:A): A
      def unit: A
    }
    
    implicit val stringMonoid: Monoid[String] = new Monoid[String] {
      def add(x: String, y: String): String = x concat y
      def unit: String = ""
    }
    
    implicit val intMonoid: Monoid[Int] = new Monoid[Int] {
      def add(x: Int, y: Int): Int = x + y
      def unit: Int = 0
    }
    
    def sum[A](xs: List[A])(implicit m: Monoid[A]): A =
      if (xs.isEmpty) m.unit
      else m.add(xs.head, sum(xs.tail))
    
    // 컴파일러가 은근 슬쩍 implicit 지정된 놈으로 후루룩뚝딱 알려줌
    println(sum(List(1, 2, 3))) // 6
    println(sum(List("a", "b", "c"))) // abc
    

묵시적 변환

  • 개요
    • 타입 S -> 타입 T로 자동 변환
    • 식의 타입이 S 인데, 기대타입이 T인 경우,
    • S => T 형태의 함수가 implicit 값을 있는 걸 사용
    • 스칼라가 몰래몰래 형 변환 필요할 때 쓱 해버림
  • 예시
    implicit def list2ordered[A](x: List[A])
      (implicit elem2ordered: A => Ordered[A]): Ordered[List[A]] = {
        def compare(that: List[A]): Int = 1
    }
    
    List(1, 2, 3) <= List(4, 5)
    
    "Hello World".take(5) // 이런것도 String => StringOps로 묵시적 변환해줌
    

타입 추론

  • 컴파일러가 타입 때려 맞춰줌
    • 리턴할 친구가 너무 명확하면 타입 안써줘도 됨
  • 동적 언어 마냥 그냥 쓸 수 있지만, 언어적 차원에서 엄한거 넘기면 컴파일 에러!
  • 너무 확정적인걸로 하면 변경하기 좀 어려울 수 있음
    val businessName: String = "Connectable"
    
    def squareOf(x: Int) = x * x
    
    def fac(n: Int) = if (n == 0) 1 else n * fac(n - 1)
    
    case class MyPair[A, B](x: A, y: B)
    val p = MyPair(1, "scala")
    
    def id[T](x: T) = x
    val q = id(1)
    
    Seq(1, 3, 4).map(x => x * 2)