lambda

스칼라에서의 lambda 식은 다음과 같다. 

( x : Int ) => Int 

이것은 Int 형을 받아서 String 을 리턴하는 함수에 대한 람다식이다.


object lambdaTest extends App {

def test = (x:Int) => x + 1

log (test(3).toString())

}

위의 test 메소드는 다음과 같다.

def test (x: Int):Int = {
x + 1
}


다음 코드를 살펴보자.  함수의 매개변수로 람다식이 사용되었다. 

object lambdaTest extends App {

def test (f: Int => String) {
log(f(3))
}

def logging (x: Int ):String = {
x.toString()
}

test(logging)

}

 - test 함수는 Int 를 매개변수로 갖고 String 을 리턴해주는 어떤 함수를 매개변수로 받는다.
-  test 함수에 들어갈 매개변수인  logging 함수는 보는 바와 같이 Int 형을 매개변수로 갖고 String 을 리턴해주는 함수이다.


다음 코드는 좀 더 복잡하게 꼬아 놓았다.
apply 메소드는 매개변수로 함수를 받는데 그 함수의 모양이 String 을 받아서 String 을 리턴해주는것이 아니라 String 을 받아서 다시 함수를 리턴해 준다. 그 함수는 String 을 받아서  String 을 리턴해주는 함수이다.
사실 글로써 설명하니깐 더 헥깔릴거라 본다. ;;

따라서 람다식의 모양이 x => y => x + y 이 되었다.
설명하자면 x 를 매개변수로 받아서 y => x + y 라는 함수를 리턴한다는 것이다. 


object Test {
type HandlerProps = String => String
def apply(f : String => HandlerProps): Unit = {
val g = f("HELLO")
log(g("world"))
}
}


object lambdaTest extends App {

Test{
x => y => x + y
}
} 결과 : HELLOworld

실제 프로젝트에서는 이런 함수합성에 관한 코드를 많이 보게 될 것이기 때문에 익숙해져야한다.


1. Future
2. Promise
3. Awiat
4. async
5. Observable
6. 병렬 

Future

스칼라에서의 Future 는 꽤 다양한 방식으로 사용 할 수있는데 

먼저 스칼라에서의 Future 모양을 살펴보자.

trait Future[T]   

: 퓨쳐값을 나타낸다. T 타입의 실제 우리가 리턴받기 원하는 객체를 포함한다. 

def apply[T](b: =>T) (implicit e: ExecutionContext) : Future[T] 

: 퓨처 계산을 나타낸다. 실제 계산을 수행하는 함수를 매개변수로 넣어주고 있다.

: 암시적으로 ExecutionContext 가 매개변수로 들어간다. 즉 쓰레드풀을 넣어주는것. 

퓨쳐값 과 퓨쳐계산을 잘 구분해서 기억해두자.

1) 퓨쳐 실행 

object FuturesComputation extends App {
import scala.concurrent._
import ExecutionContext.Implicits.global

Future {
log(s"the future is here")
}

log(s"the future is coming")
}

2개의 로그중 어떤것이 먼저 출력되는지는 비 결정적이다. (물론 아래 log 가 먼저 찍힐 가능성이 크겠지만) Future 를 통해서 자연스럽게 첫번째 로그는 자신의 쓰레드안에서 독립적으로 실행된다. 

Future{...} 는 예상했다시피 Future.apply 메소드 호출을 생략되진 것이다. 스칼라에서 함수호출시 () 대신 {} 를 이용할 수 있다. 단 인자가 하나일 경우에만.  {..} 의 해당내용은 apply 의 매개변수로 들어 갈 것이며 Future 값을 반환 할 것이다. 

2) 퓨쳐 실행 그리고 Future 리턴 


object FuturesDataType extends App {
import scala.concurrent._
import ExecutionContext.Implicits.global
import scala.io.Source

val myFuture: Future[String] = Future {
val f = Source.fromFile("build.sbt")
try f.getLines.mkString("\n") finally f.close()
}

log(s"started reading build file asynchronously")
log(s"status: ${myFuture.isCompleted}")
Thread.sleep(250)
log(s"status: ${myFuture.isCompleted}")
log(s"status: ${myFuture.value}")

}

- 파일을 읽는 일을 Future 계산을 통해서 위임하고 바로 Future [String] 값을 리턴받는다.
- 리턴 받은 Future 에서 계산 완료가 되었는지 계속 확인 한 후에 (즉 계속 폴링) 실제 결과 값을 찍어준다.
- 이렇게 Future 를 사용하면 사실 비동기를 사용하는 의미가 줄어 든다.

예를들어 스타벅스에서 1분마다 카운터에 가서 계속 내 커피가 나왔는지 물어 보지 말고, 커피가 나왔으면 커피를 내 자리로 바로 배달해주도록 하는 것도 커피를 주문 할 때 알려주면 안될까?

자 그 역할을 하는 콜백함수를 Future 에 넣어보자.

3) 퓨쳐 와 콜백 

object FuturesCallbacks extends App {
import scala.concurrent._
import ExecutionContext.Implicits.global
import scala.io.Source

def getUrlSpec(): Future[Seq[String]] = Future {
val f = Source.fromURL("http://www.w3.org/Addressing/URL/url-spec.txt")
try f.getLines.toList finally f.close()
}

val urlSpec: Future[Seq[String]] = getUrlSpec()

def find(lines: Seq[String], word: String) = lines.zipWithIndex collect {
case (line, n) if line.contains(word) => (n, line)
} mkString("\n")

urlSpec foreach {
lines => log(s"Found occurrences of 'telnet'\n${find(lines, "telnet")}\n")
}

urlSpec foreach {
lines => log(s"Found occurrences of 'password'\n${find(lines, "password")}\n")
}

log("callbacks installed, continuing with other work")
Thread.sleep(1000 * 10)

}

- 웹페이지를 긁어 오는 Future 를 실행하고 퓨처값[리스트]을 리턴 받는다.
-  foreach 메소드를 통해서 최종완료된 값을 처리해 준다. (콜백처리) 
- 가져온 데이터에서 "telnet" , "password" 가 있는 라인만 추려서 출력해준다.

3) 퓨쳐 와 예외 


object FuturesFailure extends App {
import scala.concurrent._
import ExecutionContext.Implicits.global
import scala.io.Source

val urlSpec: Future[String] = Future {
Source.fromURL("http://www.w3.org/non-existent-url-spec.txt").mkString
//Source.fromURL("http://www.w3.org/Addressing/URL/url-spec.txt").mkString

}

// urlSpec foreach {
// value => log(s"Found occurrences of 'telnet'\n${value}\n")
// }

urlSpec.failed foreach {
case t => log(s"exception occurred - $t")
}

Thread.sleep(1000*10)
}

- 보통 Future 계산에서 예외를 던지면 그에 대응하는 Future 값은 값으로 완료 될 수 없다.
- 스칼라 Future 는 성공적으로 완료되거나, 실패로 완료된다. 퓨처가 실패로 완료되는 경우를 퓨쳐가 실패 했다라고 말한다. 
- foreach 메소드는 성공적으로 완료한 퓨처에서만 값을 받는 콜백만을 받는다. 
- 따라서 실패 콜백을 지정하기 위해서는 다른 메소드가 필요하다. 그게 failed 라 한다. 
- failed 메소드는 퓨처가 실패하면서 내놓는 예외를 포함하는 Future[Throwable] 객체를 반환한다. 


object FuturesExceptions extends App {
import scala.concurrent._
import ExecutionContext.Implicits.global
import scala.io.Source
import scala.util.{Try, Success, Failure}


val file = Future { Source.fromFile(".gitignore-SAMPLE").getLines.mkString("\n") }

file foreach {
text => log(text)
}

file.failed foreach {
case fnfe: java.io.FileNotFoundException => log(s"Cannot find file - $fnfe")
case t => log(s"Failed due to $t")
}


file onComplete {
case Success(text) => log(text)
case Failure(t) => log(s"Failed due to $t")
}

Thread.sleep(1000*10)
}

- 간결하게 onComplete 를 통해서도 성공과 실패를 처리 함께 할 수 있다.
- onComplete 함수안에는 Try[T] 가 패턴매칭으로 들어간다.


object FuturesTry extends App {
import scala.util._

val threadName: Try[String] = Try(Thread.currentThread.getName)
val someText: Try[String] = Try("Try objects are created synchronously")
val message: Try[String] = for {
tn <- threadName
st <- someText
} yield s"$st, t = $tn"

message match {
case Success(msg) => log(msg)
case Failure(error) => log(s"There should be no $error here.")
}

Thread.sleep(1000*10)
}

- Try[T] 타입은 Option[T] 와 비슷하지만 다른점은 실패시 정보를 추가 할 수 있다는 점이다. Option[T] 같은 경우 그냥 None 일 뿐이지 않는가.
- Try[T] 는 Future[T] 와 달리 동기적으로 다루어진다.

4) 퓨쳐 와 합성

map

콜백은 유용하지만 프로그램이 커지면 이를 사용한 프로그램 흐름에 대한 추론이 어려워 진다.
또한 콜백을 사용하면 특정 패턴의 비동기 프로그래밍을 사용할 수 없게 된다. 특히 여러 퓨처에 한꺼번에 한 콜백을 등록하는 것은 귀찮은 일이 된다.


val buildFile = Future { Source.fromFile("build.sbt").getLines }

val longestBuildLine = buildFile.map(lines => lines.maxBy(_.length))

longestBuildLine onComplete {
case Success(line) => log(s"the longest line is '$line'")
}

map 은 기존 퓨처에 있는 값을 사용해서 새 퓨처를 만드는 메소드이다. 
def map[S](f: T => S) (implicit e : ExecutionContext) : Future[S] 

위의 예에서는 1. build.sbt 파일에서 각 라인들을 읽어서 2. 그 중에서 가장 긴 라인을 출력하는 2가지의 함수가 합성되었다.이걸 쓰레드를 직접만들어서 했을 때와 비교하면 얼마나 간단해 졌는지 실감 할 수 있을 것이다.

for complehension

val gitignoreFile = Future { Source.fromFile(".gitignore-SAMPLE").getLines }

val longestGitignoreLine = for (lines <- gitignoreFile) yield lines.maxBy(_.length)

longestBuildLine onComplete {
case Success(line) => log(s"the longest line is '$line'")
}

다음처럼 for comprehension 으로 처리 할 수 도 있다.
스칼라에서는 map 메소드가 있는 객체에 대해서는 for comprehension 을 허용한다.

async - await

아래 처럼 Future 를 async-await 구문을 사용하여, 명령형 언어처럼 순차적으로 쓸 수 있어서 가독성이 증가 된다.

import scala.concurrent.ExecutionContext.Implicits.global import scala.async.Async._ val gitignoreFile1: Future[Int] = ... val gitignoreFile2: Future[Int] = ... val asyncComputation = async { await(gitignoreFile1) + await(gitignoreFile2)

}

flatMap

먼저 스칼라 List 에서의 map 과 flatMap의 차이를 살펴보자.

scala> val fruits = Seq("apple", "banana", "orange")
fruits: Seq[java.lang.String] = List(apple, banana, orange)

scala> fruits.map(_.toUpperCase)
res0: Seq[java.lang.String] = List(APPLE, BANANA, ORANGE)

scala> fruits.flatMap(_.toUpperCase)
res1: Seq[Char] = List(A, P, P, L, E, B, A, N, A, N, A, O, R, A, N, G, E)

map 은 받아드린 리스트의 하나의 값에 대해서 따로 실행하여 개별 List 를 만들었고
flatMap 은 따로 실행하여 큰 뭉치의 List 로 만들었다. 즉 하나의 큰 List 로 평평하게 펴서 나열했다는 의미. 

또 다른 flatMap 의 특징을 알아보자.

def toInt(s: String): Option[Int] = {
    try {
        Some(Integer.parseInt(s.trim))
    } catch {
        // catch Exception to catch null 's'
        case e: Exception => None
    }
}

scala> val strings = Seq("1", "2", "foo", "3", "bar")
strings: Seq[java.lang.String] = List(1, 2, foo, 3, bar)

scala> strings.map(toInt)
res0: Seq[Option[Int]] = List(Some(1), Some(2), None, Some(3), None)

scala> strings.flatMap(toInt)
res1: Seq[Int] = List(1, 2, 3)

scala> strings.flatMap(toInt).sum
res2: Int = 6
 None 은 무시하고 Some 타입만, 타입을 벗겨서 처리하고있다.

이제 Future 에서 어떻게 flatMap을 정의하였는지 살펴보자. 

val netiquette = Future { Source.fromURL("http://www.ietf.org/rfc/rfc1855.txt").mkString }
val urlSpec = Future { Source.fromURL("http://www.w3.org/Addressing/URL/url-spec.txt").mkString }
val answer = netiquette.flatMap { nettext =>
urlSpec.map { urltext =>
"First, read this: " + nettext + ". Now, try this: " + urltext
}
}

answer foreach {
case contents => log(contents)
}

FlatMap 을 사용하였다. 이것은 Map과는 다르게 현재 퓨처가 완료되고 그 결과값에 f 를 적용해서 생기는 새로운 퓨처가 완료되어야만 완료된다. 즉 아래와 같다.

def map[S](f: T => S) (implicit e : ExecutionContext) : Future[S] 

def flatMap[S](f: T => Future[S]) (implicit e : ExecutionContext) : Future[S] 

위의 코드는 결국 nettext 라는 값을 A퓨처의 결과로받고, urltext 라는 값 또한 B라는 퓨처의 결과로 받아서     하나의 처리를 거친 후에 answer 라는 새로운  Future[S] 를 반환한다는것을 말해주고있다. 

object FuturesFlatMap extends App {
import scala.concurrent._
import ExecutionContext.Implicits.global
import scala.io.Source

val netiquette = Future { Source.fromURL("http://www.ietf.org/rfc/rfc1855.txt").mkString }
val urlSpec = Future { Source.fromURL("http://www.w3.org/Addressing/URL/url-spec.txt").mkString }
val answer = for {
nettext <- netiquette
urltext <- urlSpec
} yield {
"First of all, read this: " + nettext + " Once you're done, try this: " + urltext
}

answer foreach {
case contents => log(contents)
}

Thread.sleep(1000*10)
}

보기 간단하게 for comprehesion 으로 바꾸었다.

for comprehension 보다 더 이해하기 쉬운 방법으로는 async, awit 가 있는데 여기서는 생략한다.

다음과 같은 Data Flow 로 그려볼 수 있다.  Reactive 파라다임에서 말하는  
"모든 지점에서 블럭 되지 않게 하자. 자연스럽게 이벤트가 흘러다니도록 하자." 
이 되겠다.

5) 이후에 알아야 할 것들

기존 콜백기반의API와 퓨처사이에 다리를 놓고 잘 조합하기 위해서는 퓨처계산을 원하는 대로 정의할수있는 Promise를 알아야한다. 또한 퓨처들의 종료를  기다렸다가 무언가를 처리해야 하는 경우에 (블록이 가끔 필요하다) Await.readyAwait.result 등도 알아야한다.

위의 모든것들을 편하게 사용하게 하는 스칼라 비동기 라이브러리가 있다. 바로 asyncawait 인데 이것을 사용하면 조금은 더 편하게 Future 를 활용할 수 있다. 

마지막으로 결과를 한번만 처리 할 수 있는 퓨처말고 동일한 계산으로 부터 여러 번 서로 다른 이벤트를 받아야 하는 경우 ( 파일 다운로드 상태 추적등) 에는 이벤트 스트림에 대해서 알아야한다. 이러한 이벤트 스트림을 Observable[T] 라는 타입으로 표현했는데 이에 관한 내용도 알아야한다. Akka Streaming 이라는 도구도 있다.

6) 한꺼번에 이해하기 

아래 카카오에서 발행한 글을 통해 다시 복습해서 이해해보면 좋을 거 같다.

카카오톡 - 스칼라 Future 를 통한 모나드 이해 

7) 비동기식 퓨처 연산 정리 

  설명

 예제 

 설명 

 failbackTo 

 nextFtr(1) failbackTo nextFtr(2) 

 두번째 퓨쳐를 첫번째 연결하고 새로운 종합적인 퓨처를 반환함. 첫 번째 퓨처가 성공적이지 않다면 두 번째 퓨처가 호출됨 

 flatMap 

 nextFtr(1).flatMap(int => nextFtr()) 

 두번째 퓨처를 첫 번째에 연결하고 새로운 종합적인 퓨처를 반환함. 첫 번째가 성공적이라면 그 반환 값이 두 번째를 호출하는데 사용됨 

 map 

 nextFtr(1) map (_ * 2) 

 주어진 함수를 퓨처에 연결하고 새로운 종합적인 퓨처를 반환함. 퓨처가 성공적이라면 그 반환 값이 해당 함수를 호출할 때 사용됨 

 onComplete 

 nextFtr() onComplete
  { _ getOrElse 0 }  

 퓨처의 작업이 완료된 후 주어진 함수가 값 또는 예외를 포함한 Try를 이용하여 호출됨 

 onFailure 

 * 2.12 부터 사장됨 

 

 onSuccess  

 * 2.12 부터 사장됨 

 

 Future.sequence

 concurrent.Future sequence List(nextFtre(1),nextFtr(5))  

 주어진 시퀀스에서 퓨처를 벙행으로 실행하여 새로운 퓨처를 반환함. 시퀀스 내의 모든 퓨처가 성공하면 이들의 반환값의 리스트가 반환됨. 그렇지 않으면 그 시퀀스내에서 처음으로 발생한 예외가 반환됨  

* 러닝스칼라 (제이슨스와츠,제이펍 발생) 에서 발췌


레퍼런스:

1.스칼라 동시성 프로그래밍

2.러닝 스칼라 


스칼라에서의 가변인자 


1. 간단 예제  

object test {

def func[T](xs:T*) = xs.foreach(x => println(x))

def main(arg : Array[String]): Unit ={
func(0,1,2)
}
}

(xs : T*)   처럼 타입뒤에 * 를 붙혀주면 됩니다.


2. 컬렉션은  _* 를 붙여줍니다.

object test {

def sum(xs:Int*):Int = if (xs.isEmpty) 0 else xs.head + sum(xs.tail:_*)

def main(arg : Array[String]): Unit ={

println(sum(0,1,2,3)) // OK !

val ns = List(1, 3, 5)
println(sum(ns:_*)) // OK!

println(sum(List(1, 3, 5))) // Not Good
}
}

 : _* 를 붙여줌으로써 컴파일러에게 가변인자라는것을 알려줍니다.


스칼라에서 사용되는 심볼들 

http://stackoverflow.com/questions/7888944/what-do-all-of-scalas-symbolic-operators-mean


나는 교육을 위한 목적으로 4개의 카테고리로 연산자들을 나눕니다.

  • Keywords/reserved symbols
  • Automatically imported methods
  • Common methods
  • Syntactic sugars/composition

다행히도 대부분의 범주는 다음과 같은 질문에서 나타납니다:

->    // Automatically imported method
||=   // Syntactic sugar
++=   // Syntactic sugar/composition or common method
<=    // Common method
_._   // Typo, though it's probably based on Keyword/composition
::    // Common method
:+=   // Common method

대부분의 메소드의 정확한 의미는 그것들을 정의하는 클래스에 따라 다릅니다. 예를 들어, Int 의 <= 는 "보다 작거나 같음"을 의미합니다. 위 박스의 첫 번째 것, -> 는 아래에 예를 들어 보겠습니다. :: 는 아마도 List에 정의 된 메소드 일 것입니다. (같은 이름의 객체 일 수도 있지만)  : + = 는 아마도 다양한 Buffer 클래스에 정의 된 메소드 일 것입니다.

자 구체적으로 보시죠


Keywords/reserved symbols

스칼라에는 특별한 기호가 있습니다. 그 중 두 개는 적절한 키워드로 간주되는 반면 다른 키워드들은 단지 "예약 된"키워드입니다.

// Keywords
<-  // Used on for-comprehensions, to separate pattern from generator
=>  // Used for function types, function literals and import renaming

// Reserved
( )        // Delimit expressions and parameters
[ ]        // Delimit type parameters
{ }        // Delimit blocks
.          // Method call and path separator
// /* */   // Comments
#          // Used in type notations
:          // Type ascription or context bounds
<: >: <%   // Upper, lower and view bounds
<? <!      // Start token for various XML elements
" """      // Strings
'          // Indicate symbols and characters
@          // Annotations and variable binding on pattern matching
`          // Denote constant or enable arbitrary identifiers
,          // Parameter separator
;          // Statement separator
_*         // vararg expansion
_          // Many different meanings

이것들은 모두 언어의 일부이며, Scala Specification(PDF 자체와 같이 언어를 적절하게 기술하는 텍스트에서 찾을 수 있습니다.

마지막의 밑줄은 특별한 설명이 필요합니다. 왜냐하면 널리 사용되기 때문에 여러 가지 의미가 있기 때문입니다. 다음은 샘플입니다.

import scala._    // Wild card -- all of Scala is imported
import scala.{ Predef => _, _ } // Exception, everything except Predef
def f[M[_]]       // Higher kinded type parameter
def f(m: M[_])    // Existential type
_ + _             // Anonymous function placeholder parameter
m _               // Eta expansion of method into method value
m(_)              // Partial function application
_ => 5            // Discarded parameter
case _ =>         // Wild card pattern -- matches anything
f(xs: _*)         // Sequence xs is passed as multiple parameters to f(ys: T*)
case Seq(xs @ _*) // Identifier xs is bound to the whole matched sequence


Automatically imported methods

따라서 위의 목록에서 찾고있는 기호를 찾지 못했다면 메소드 또는 그것의 일부 여야합니다. 그러나 종종 기호를 볼 수 있으며 클래스의 설명서에는 해당 방법이 없습니다. 이 경우 다른 메소드로 하나 이상의 메소드 컴포지션을 보거나 메소드를 범위로 가져 오거나 가져온 암시 적 변환을 통해 사용할 수 있습니다.

These can still be found on ScalaDoc: you just have to know where to look for them. Or, failing that, look at the index (presently broken on 2.9.1, but available on nightly).

Every Scala code has three automatic imports:

// Not necessarily in this order
import _root_.java.lang._      // _root_ denotes an absolute path
import _root_.scala._
import _root_.scala.Predef._

The first two only make classes and singleton objects available. The third one contains all implicit conversions and imported methods, since Predef is an object itself.

Looking inside Predef quickly show some symbols:

class <:<
class =:=
object <%<
object =:=

Any other symbol will be made available through an implicit conversion. Just look at the methods tagged with implicit that receive, as parameter, an object of type that is receiving the method. For example:

"a" -> 1  // Look for an implicit from String, AnyRef, Any or type parameter

In the above case, -> is defined in the class ArrowAssoc through the method any2ArrowAssocthat takes an object of type A, where A is an unbounded type parameter to the same method.


Common methods

So, many symbols are simply methods on a class. For instance, if you do

List(1, 2) ++ List(3, 4)

You'll find the method ++ right on the ScalaDoc for List. However, there's one convention that you must be aware when searching for methods. Methods ending in colon (:) bind to the right instead of the left. In other words, while the above method call is equivalent to:

List(1, 2).++(List(3, 4))

If I had, instead 1 :: List(2, 3), that would be equivalent to:

List(2, 3).::(1)

So you need to look at the type found on the right when looking for methods ending in colon. Consider, for instance:

1 +: List(2, 3) :+ 4

The first method (+:) binds to the right, and is found on List. The second method (:+) is just a normal method, and binds to the left -- again, on List.


Syntactic sugars/composition

So, here's a few syntactic sugars that may hide a method:

class Example(arr: Array[Int] = Array.fill(5)(0)) {
  def apply(n: Int) = arr(n)
  def update(n: Int, v: Int) = arr(n) = v
  def a = arr(0); def a_=(v: Int) = arr(0) = v
  def b = arr(1); def b_=(v: Int) = arr(1) = v
  def c = arr(2); def c_=(v: Int) = arr(2) = v
  def d = arr(3); def d_=(v: Int) = arr(3) = v
  def e = arr(4); def e_=(v: Int) = arr(4) = v
  def +(v: Int) = new Example(arr map (_ + v))
  def unapply(n: Int) = if (arr.indices contains n) Some(arr(n)) else None
}

val Ex = new Example // or var for the last example
println(Ex(0))  // calls apply(0)
Ex(0) = 2       // calls update(0, 2)
Ex.b = 3        // calls b_=(3)
// This requires Ex to be a "val"
val Ex(c) = 2   // calls unapply(2) and assigns result to c
// This requires Ex to be a "var"
Ex += 1         // substituted for Ex = Ex + 1

The last one is interesting, because any symbolic method can be combined to form an assignment-like method that way.

And, of course, there's various combinations that can appear in code:

(_+_) // An expression, or parameter, that is an anonymous function with
      // two parameters, used exactly where the underscores appear, and
      // which calls the "+" method on the first parameter passing the
      // second parameter as argument.



Type projection


개요:

타입 안의 (nested) 타입 멤버를 레퍼런싱 하기 위한 문법이다.
T#x 라고 지칭하며, 타입 T 안의 x 라는 이름의 타입 멤버를 나타낸다.


예제: 


아래에 보면 클래스 내부에 또 하나의 클래스 (nested class) 가 있는 것을 볼 수 있다.

class A {
  class B

  def f(b: B) = println("Got my B!")
}

 아래와 같이 시도해보면 

scala> val a1 = new A
a1: A = A@2fa8ecf4

scala> val a2 = new A
a2: A = A@4bed4c8

scala> a2.f(new a1.B)
<console>:11: error: type mismatch;
 found   : a1.B
 required: a2.B
              a2.f(new a1.B)
                   ^

클래스 내부의 클래스를 정의하는데 A.B 는 안되며,  a1.B 와 a2.B 로 정의하는데, 보는 바와 같이 a1 과 a2 각각 가지고있는 class B 는 동일하지 않다.


여기서 #  의 쓰임새가 나오는데 # 은 다른 nested 클래스에 대한 참조를 할 수 있도록 만들어준다. 
A.B 아닌 A#B 로 말이다. 이것의 의미는 아무 A 기반의 인스턴스 내의 B 클래스를 말한다.

다시 만들어서 시도해보면 

class A {
  class B

  def f(b: B) = println("Got my B!")
  def g(b: A#B) = println("Got a B.")
}
scala> val a1 = new A
a1: A = A@1497b7b1

scala> val a2 = new A
a2: A = A@2607c28c

scala> a2.f(new a1.B)
<console>:11: error: type mismatch;
 found   : a1.B
 required: a2.B
              a2.f(new a1.B)
                   ^

scala> a2.g(new a1.B)
Got a B.

잘된다.


참고) 


http://stackoverflow.com/questions/9443004/what-does-the-operator-mean-in-scala

스칼라에서의 for - comprehensions


1. 개념 

스칼라 Doc 에서는 이렇게 말합니다. (http://docs.scala-lang.org/tutorials/FAQ/yield.html)

파이썬,루비등에 있는 yield 처럼 스칼라도 yield 를 가지고 있지만 좀 다릅니다. 스칼라의 yield 는 for comprehensions 의 일부분으로 사용되며, 다른 언어의 list-comprehensions 의 일반화 입니다. 스칼라의 "for comprehensions" 는 하스켈의 "do" 와 동등하며  멀티플모나딕 연산을 위한 사용편의 정도일 뿐입니다.

사실 for - comprehensions 같은것들은  syntactic sugar 라고 합니다. 번역하면 사용자가 편하게 사용하기 위한 사탕발림 쯤 되는데요. 일단 외우는세요.  자바나 C++에서 스칼라로 넘어 왔을때 사실 모든게 syntatic sugar 로 보이긴하죠 ㅎㅎ

스칼라 for - comprehensions 를 다양한곳에서 가져온 예제를 통해서 살펴보겠습니다. (개발자는 코드로 말합니다. 이해안가면  코드만 계속 반복해서 보세요. 각성 하실 겁니다. ) 

혹시 python 의 yield 를 사용했던 분이라면 그것은 순회하면서 하나씩 바깥으로 던지는데 반해, scala yield 는 모아뒀다가 컬렉션으로 던저 주는 느낌으로 받아 드리면 될 거 같습니다.


예제 0) flatMap  이란?

scala> val nestedNumbers = List(List(1, 2), List(3, 4)) scala> nestedNumbers.flatMap(x => x.map(_ * 2)) res0: List[Int] = List(2, 4, 6, 8)

하나의 리스트로 '합쳐' 준다는 것을 유념하세요. 그냥 map 을 적용시키면 List(2,4) List(6,8) 이렇게 나오겠죠.
flatMap 이 스칼라나 함수형 프로그래밍에서 굉장히 중요합니다.

F[A]에 대한 Monad는 아래 함수를 가진다

  • (F[A], A => F[B]) => F[B] 타입을 가진 flatMap 메소드
    (즉 A를 감싼F타입에서 A를 빼내어 B로 만든후 F 타입으로 다시 감싼다) 
  • A => F[A] 타입을 가진 pure 메소드 (A 를 감싼 타입으로 변경) 

모나드는 3개의 법칙을 따라야 한다.

  1. Left identity: (pure(a) flatMap f) == f(a)
  2. Right identity: (m flatMap pure) == m
  3. Associativity: (m flatMap f flatMap g) == (m flatMap (x => f(x) flatMap g))

모나드는 pure flatMap을 가지며 결합법칙 항등법칙 만족하도록 만든 구현이다. 즉 이 두 함수들을 이용해서 변수의 도입과 결속 그리고 변수 치환수행을 위한 문맥을 제공한다고 볼수 있으며 이 두 함수를 이용해서 많은 함수를 파생 시킬 수 있으며 코드를 줄이는 것도 가능하다


http://www.bench87.com/content/24 발췌 (COMET)

이런것들이 이해 안가면 예제만 반복해서 봅니다. 그럼 이해갑니다. 인간의 능력이란 반복에서 나옵니다.

예제 1)

  1. for(x <- c1; y <- c2; z <-c3) {...}

위의 문장은 아래 처럼 사용 될 수 있습니다.

  1. c1.foreach(x => c2.foreach(y => c3.foreach(z => {...})))

c1의 요소가 3개 , c2 가 3개,  c3 가 3개라면 27번의 행위가 발생되겠군요.


예제 2)

  1. for(x <- c1; y <- c2; z <- c3) yield {...}

위의 문장은 아래 처럼 사용 될 수 있습니다. 

  1. c1.flatMap(x => c2.flatMap(y => c3.map(z => {...})))

예제 3)

  1. for(x <- c; if cond) yield {...}

위의 문장은 아래 처럼 사용 될 수 있습니다.

  1. c.withFilter(x => cond).map(x => {...})

cond 에 합당하는 x 들을 이용하여 어떤 행위를 한 후에 다시 c 타입을 갖게 합니다.


예제 4)

  1. for(x <- c; y = ...) yield {...}

위의 문장은 아래 처럼 사용 될 수 있습니다.

  1. c.map(x => (x, ...)).map((x,y) => {...})

map 을 대신해서 사용 할 수 있지만 아래 예를 보시면 느껴지다 시피 무엇이 더 이해하기 쉽습니까?
괄호가 연속된 map 보다는 for yield 를 사용한게 훨씬 명료하네요.


  1. l.flatMap(sl => sl.filter(el => el > 0).map(el => el.toString.length))

or

  1. for{
  2. sl <- l
  3. el <- sl
  4. if el > 0
  5. } yield el.toString.length


예제 5) 

scala> val capNames = for (e <- names) yield e.capitalize
capNames: Array[String] = Array(Chris, Ed, Maurice)

for 문을 통해서 하나씩 실행한 후에 yield 를 통해서 names 의 capitalize 만 따로 추출해서 새로운 배열로 만들어집니다.

scala> val lengths = for (e <- names) yield {
     |   // imagine that this required multiple lines of code
     |   e.length
     | }
lengths: Array[Int] = Array(5, 2, 7)

이번에는 숫자를 추출해서 그것으로 배열이 만들어 졌습니다.


예제 6) 

scala> val s = for {
     |  x <- 1 to Integer.MAX_VALUE
     |  if(x <= 10)
     | } yield 2*x

s: (2, 4, 6, 8, 10, 12, 14, 16, 18, 20)

모든 숫자중에서 10보다 작은 숫자를 2배해서 set s 에 담아줬습니다.


예제 7)

  1. def eitherExample(num: Some(Int)): Either[String, Int] = num match {
  2.   case Some(n) => Right(n)
  3.   case None => Left("Error! Number is missing!")
  4. }

입력이 합당하면 Either 의 오른쪽에 값(n) 을 넣고 아니면 왼쪽에 에러를 출력 합니다.
match case 를 이용하였습니다.

  1. val x: Either[String, Long] = Right(7)
  2. x.right.map(num => println(num))
  3. x.left.map(msg => println(msg))


Either 의 오른쪽에 값이 있는 x 를 정의하고 오른쪽 왼쪽을 사용합니다. 위에서 map 은 값이 없는 경우라면 무시할 것입니다. 안전하게 사용 할 수 있겠네요.

  1. val x: Either[String, Int] = Right(7)
  2. val result1 = for( res <- x.right ) { res }
  3. val result2 = for( res <- x.left) { res }


이렇게 for 를 사용할 수도 있습니다. 값이 있는곳 만 처리 됩니다.

  1. for {
  2.   id <- x.right
  3.   name <- y.right
  4. } yield {
  5.   val level = lookupLevel(id)
  6.   Member(id, name, level)
  7. }.fold(
  8.   err => NotAuthorized(err),
  9.   member => member
  10. )

마지막으로 for - yield 문을 보시면 Either 의 x, y 값을 id , name 으로 담습니다.
해당 값들에 문제가 없을 경우에 yield 가 실현됩니다. 여기선
 Member 객체를 만들죠. 그 후에 fold 를 통해서
문제가 있다면 첫번째 함수를 따르고 문제가 없다면 두번째 member => member 를 실행해서 결국 member 가 리턴됩니다. 


예제 8)

case class Postcard(msg: String)

def sendPostcards: List[Postcard] = {
    val states = List("Arizona", "New Mexico",
                          "Texas", "Louisiana",
                          "Mississippi", "Virginia",
                          "New York", "Ohio",
                          "Illinois")
    val relatives = List("Grandma", "Grandpa", "Aunt Dottie", "Dad")
    val travellers = List("Kelsey", "DJ")

    var postcardList: List[Postcard] = List()

    for (h <- 0 until travellers.length) {
        val sender = travellers(h)

        for (i <- 0 until relatives.length) {
            val recipient = relatives(i)

            for (j <- 0 until states.length) {
                val theState = states(j)
                postcardList ::=
                    new Postcard("Dear " + recipient + ", " +
                                  "Wish you were here in " +
                                  theState + "! " +
                                  "Love, " + sender)
            }
        }
    }

    postcardList
}


위의 예제를 아래처럼 for - yield 를 사용해서 바꿔 줄 수 있습니다. (정확히 동일하지 않습니다) 

def sendPostcards3: List[Postcard] = {
    val states = List("Arizona", "New Mexico",
                          "Texas", "Louisiana",
                          "Mississippi", "Virginia",
                          "New York", "Ohio",
                          "Illinois")
    val relatives = List("Grandma", "Grandpa", "Aunt Dottie", "Dad")
    val travellers = List("Kelsey", "DJ")

    for {
        traveller <- travellers
        sender = traveller + " (your favorite)"
        relative <- relatives
        theState <- states
        if (relative.startsWith("G"))
    } yield {
        new Postcard("Dear " + relative + ", " +
                        "Wish you were here in " +
                         theState + "! " +
                         "Love, " + sender)
    }
}

각각의 리스트들을 for 문 안에서 하나를 가져와서 if 문을 통해 데이터를 솎아내고, 그 데이터들을 이용하여   yield 를 통해서 하나의 객체를 만든 후에 다시 순회하면서 객체의 List 를 리턴해 주고 있습니다.

각각의 리스트가 5,5,5 개라면 최대 5*5*5 만큼의 객체가 생성될것입니다.

* 참고로 커링이 무엇인가요에 대한 대답은 너무 쉽다. 하지만  왜 커링이 그렇게 유용하나요? 에 대한 대답은 아직 못얻었다. 뇌 로는 알겠지만 체득하지 못한 상태.  스칼라를 함수형 파라다임으로 사용을 많이 해봐야 알게 되지 않을까..

스칼라에서의 커링 (currying)


1. 개념 

스칼라 Doc 에서는 이렇게 말합니다.  (http://docs.scala-lang.org/ko/tutorials/tour/currying)

메소드에는 파라미터 목록을 여럿 정의할 수 있다. 파라미터 목록의 수 보다 적은 파라미터로 메소드가 호출되면, 해당 함수는 누락된 파라미터 목록을 인수로 받는 새로운 함수를 만든다.

중요한 포인트를 뽑아내어 쉽게 설명해보면 

매우 쉬운 설명 

 - 메소드라는건 파라미터를 가지고 있지요?  예를들어  add( int x , int y)  이렇게 2개를 가지고 있을때
 - add 라는 메소드를 호출 할 때 인자 2개를 넣어주지 않으면 보통 에러 나잖습니까?
 - 근데 하나만 넣어도 에러가 나지 않는데다가 ~~   즉 add (10)  이렇게 해두 되고 
 - 누락된 파라미터 만을 사용하는 새로운 메소드를 만들어 준 답니다.~ 즉 add(int y) 라는 새 메소드말이죠.
 - add (int y) 라는 새 메소드는 나중에 호출 할 수 있는데 그 때 이전에 넣어 둔 10이 암시적으로 작용됩니다.

 슈도코드로 살짝 보면 

 int add (int x, int y) {
   return x + y;
 }

원래 이런 함수인데
 add (10) 이렇게 호출하면 add(int y) 라는 메소드가 만들어지며 , 10은 새 메소드에 저장되어 있게 됩니다.

int add( int y) {
    return 10 + y;
}

즉 이런 함수가 자동으로 만들어 진 다는 말이지요.
이 함수를 add(5) 호출하면 결과로 이전에 입력된 10을 더하여 15가 리턴됩니다.

2. 스칼라 예제

  1. object CurryTest extends App {
  2. def filter(xs: List[Int], p: Int => Boolean): List[Int] =
  3. if (xs.isEmpty) xs
  4. else if (p(xs.head)) xs.head :: filter(xs.tail, p)
  5. else filter(xs.tail, p)
  6. def modN(n: Int)(x: Int) = ((x % n) == 0)
  7. val nums = List(1, 2, 3, 4, 5, 6, 7, 8)
  8. println(filter(nums, modN(2)))
  9. println(filter(nums, modN(3)))
  10. }

위의 스칼라 예제의 modN(n: Int)(x: Int) 가 커링되는 함수입니다.
보시다시피 modN(n: Int)(x: Int) 는 2개의 파라미터를 넣어야하는 함수입죠..

위에서 설명했다시피 이걸 파라미터 하나만 넣어서 호출 할 수 도 있으며 그렇게 하면 새로운 함수가 만들어진답니다..  즉 modN(2) 이렇게 호출 했더니 에러는 안나고 대신 새로운 함수를 만들어서

def filter(xs: List[Int], p: Int => Boolean)  filter 파라미터 p로 넣어주고 있습니다.

위의 p 파라미터 자리에는  modN(2) 를 호출해서 만들어진 새로운 함수가 들어 간다는 말입니다.
새로운 함수는 이렇게 생겼겠죠?  (이미 2를 n 자리에 넣었다는것을 기억하시구요) 

def modN(x: Int) = ((x % 2) == 0)

3. HOW? 

여기에 하나의 함수를 파라미터로 받아서 , 그 함수를 내부에서 사용하고 2개의 Int 형을 받는 함수를 리턴하는 함수가 있습니다.

def sum(f:Int=>Int):(Int,Int)=>Int={
   def sumF(a:Int,b:Int):Int= 
      if(a>b) 0 else f(a)+ sumF(a+1,b)
   sumF
  }

이런 경우 내부에 함수를 선언하지 않고 , 함수 파라미터 한개와 2개의 Int 형 파라미터 , 즉 3개의 파라미터를 받는 커링 함수를 아래처럼 만들수 도 있습니다.

def sum(f:Int=>Int)(a:Int,b:Int):Int=
  if(a>b) 0 else f(a)+sum(f)(a+1,b)

4. {} 의 사용 

스칼라에서는 함수호출시  ()  대신 {} 중괄호를 사용 할 수 있다.  단 !!! 인재가 하나일 경우 !!

println ("Hello World") 대신해서 println { "Hello World" } 를 사용 할 수 있다는 의미이다.

스칼라에서 이렇게 한 이유는 클라이언트 프로그래머가 중괄호 내부에 함수 리터럴을 사용하도록 하기 위해서이다. 이렇게 작성한 메소드는 호출 시 좀 더 제어 추상화 구문과 비슷해진다.

def withPrintWriter(file: File, op: PrintWriter => Unit ) {

......
}

이런 함수를 호출 할 때는 

withPrintWriter ( new File ("date.txt") , 

writer => writer.println(new java.util.Date) ) 


이렇게 할 수 있는데 , 인자가 2개이기 때문에 { } 를 사용할 수 없다.

하지만 !!!!

def withPrintWriter(file: File) (op: PrintWriter => Unit ) {
val writer = new PrintWriter(file)
try {
op (writer)

} finally {
writer.close()
}
}

이러한 함수라면 {} 를 사용가능하다.

val file = new File("date.txt")
withPrintWriter (file) {
writer => writer.println(new java.util.Date)
}

왜 그러냐면 커링을 통해서 file 을 매개변수로 먼저 넣은 새로운 함수가 생겨졌고, 

그 새로운 함수는  매개변수 하나를 필요로 하는 함수이기 때문이다.


보통 (변수,함수) 매개변수 조합일 때 많이 사용된다. 

def foldLeft[B](z: B)(op: (B, A) => B): B = {
  var result = z
  this foreach (x => result = op(result, x))
  result
}
Vector(1, 2, 3).foldLeft(Vector[Int]()) { (is, i) =>
  is :+ i * i
}

5. 추가 읽을거리 : Partial function 과의 차이 

함수 인자를 생략하는 문법들에는 

- 디폴트파라미터
- implicit 
- partial applied function
- currying 

등 이 있는데 그 중에 헤깔리기 쉬운 currying vs partial function 에 대해 살펴보자. 아래 링크를 읽으심이

http://www.vasinov.com/blog/on-currying-and-partial-function-application/

+ Recent posts