관리 메뉴

HAMA 블로그

스칼라 강좌 (14) - 함수 와 메소드 본문

Scala

스칼라 강좌 (14) - 함수 와 메소드

[하마] 이승현 (wowlsh93@gmail.com) 2016. 7. 23. 13:40

함수와 메소드

이번 포스트 내용은 지금까지 그리고 앞으로 나올 강좌중에서 가장 중요한 포스트라고 생각합니다.   
스칼라에서 함수/메소드는 그 만큼 중요합니다.  본 포스트의 내용이 불분명하거나 모자르면 반드시 
다른 곳에서 보충하시길 바랍니다.


스칼라에서 함수종류 

* 객체의 멤버로 있는 함수인 메소드
* 함수안에 정의한 내포 함수
* 함수 리터럴
* 함수 값 


메소드  


특성 

스칼라 메소드 파라미터에서 중요한 점은 이들이 val 이라는것이다.

  

  def add(b : Byte) : Unit = {

b = 1  // 파라미터가 val 이라 불가능 !

sum += b

   }


 * return 을 명시적으로 사용하지 말라. 

   스칼라는 메소드가 한 값을 계산하는 표현식인 것 처럼 생각하는걸 권장한다.

 * 메소드가 오직 하나의 표현식만 계산하는 경우 {} 중괄호를 없앨 수 있다.

   def add(b:Byte) : Unit = sum += b


 * 메소드의 결과 타입이 Unit 인 경우는 부수 효과를 위해 사용한다는 뜻이다. 

 

 * Unit 생략하고 = 를 없애서 {} 로 메소드를 나타낼 수 있는데, 이건 프로시저 처럼 보이게 하는데 
   프로시저란 오직 부수 효과를 얻기 위해서만 사용하는 메소드를 의미한다. 


   즉 def add(b: Byte) { sum += b}  처럼 사용한다는 뜻이데, 여기서 함정이 있는게 


   def add2 () { " test string " }  이건 Unit 를 리턴하며 
   def add3 () = { " test string " }  이건 String 을 리턴한다는 것이다. 


   즉 프로시저 처럼 작성하면 리턴이 무조건 Unit 이 된다는점~ 


  * 표현식 블럭 즉 {} 를 이용해서 호출도 가능하다. 블럭에서 실행된 값이 매개변수로 들어감
    (스칼라에서 이 문법 익숙해지기 전까지 매우 헤깔림) 


def formatEuro (amt : Double) = f"$amt%.2f" 

formatEuro { val rate = 1.32; 0.235 + 0.7123 + rate * 5.32 }   <-- 이런식으로 '호출'


예제 1


import scala.io.Source

object LongLines {

def processFile(filename : String, width : Int ) {
  val source = Source.fromFile(filename)
  for (line <- source.getLines())
  processLine(filename, width, line)
}

private def processLine(filename: String, width: Int, line: String)  {
  if (line.length > width)
  println(filename + ": " + line.trim)
}
}


두개의 메소드로 이루어져 있다.



지역 함수

특성 

함수안에 함수를 정의 할 수 있다.


예제 2 (예제 1 의 변형 ) 

 

import scala.io.Source

object LongLines {

def processFile(filename : String, width : Int ) {

   def processLine(filename: String, width: Int, line: String)  {
      if (line.length > width)
      println(filename + ": " + line.trim)
    }

    val source = Source.fromFile(filename)
    for (line <- source.getLines())
      processLine(filename, width, line)
  }

}


도우미 함수들은 프로그램의 네임스페이스를 오염 시킬 수 있기 때문에  자바에서는 주로 비공개 메소드를 이용해 이런 목적을 달성하지만 스칼라에서는 함수안에 함수를 정의해서 처리 하기도 한다. 

함수안의 함수는 그 외부 함수안에서만 접근 할 수 있다. 


예제 3 (예제 2 의 개선 ) 


import scala.io.Source

object LongLines {

  def processFile(filename : String, width : Int ) {

   def processLine(line: String)  {
     if (line.length > width)
      println(filename + ": " + line.trim)
   }

   val source = Source.fromFile(filename)
   for (line <- source.getLines())
     processLine(filename, width, line)
  }

}



내부의 함수에서 바깥 함수의 인자를 사용한다.



1급 계층 함수

특성 

스칼라는 1급 함수 (first class function) 를 제공한다. 1급이란 말처럼 상위계층 즉 모든게 가능한.. 할당도 가능하고 , 인자로도 넘길 수 있고, 반환도 가능한 애들 말한다. 


이들은 이름 없이 리터럴로 표기해 값 처럼 주고 받을 수 있다.

함수 리터럴은 클래스로 컴파일하는데, 해당 클래스를 실행 시점에 인스턴스화하면 함수 값이 된다.  

모든 함수 값은 FunctionN 트레이트 중 하나를 확장한 클래스로부터 만든 인스턴스이다.

예를들어 Function() 은 인자가 없는 함수고, Function1 은 인자가 1개인 식이다. 

여기에는 함수 호출시 사용하는 apply 메소드가 들어 있다.


다음 예와 같다.



1
2
3
4
5
trait Function1[ T1, R] extends AnyRef {
    ...
    def apply( v1 :T1) : R
    ...
}



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Last( init : Int ) extends Function1[Int,Int]{
    var last = init
 
    def apply( x :Int ) : Int = {
        val retval = last
        last = x
        retval
    }
}
 
val last : Int => Int   =   new Last( 1 )
 
val x = last( 5 )     // x == 1
val y = last( 5 )     // y == 5
val z = last( 8 )     // z == 5
 ...
val a = last( 5 )     // a == 8



함수 리터럴  (간단 예제)  


(x: Int) => x + 1


정수 x 를 x + 1 로 매핑하는 함수이다. 

함수 값은 객체이기 때문에 원하면 변수에 저장 할 수 있다.

동시에 함수 값은 엄연히 함수이기도 하다. ( 괄호를 이용해 실행 할 수 있다는 야그) 


var increase = (x : Int) => x + 1     //  함수를 increase 변수에 저장가능!

increase(10)  // 호출 가능 !  내부적으론 apply 메소드가 사용됨. 



함수 리터럴 ( 둘 이상의 문장 예제) 


increate = (x: Int ) => {

println("...")

println("...")

x + 1

}


둘 이상의 문장이 필요하면 {} 중괄호로 감싸서 블록으로 만든다. 



함수 리터럴 ( 매개변수로 사용) 


val numbers = List (1,2,3,4,5)

numbers .foreach((x:Int) => println(x)) 


다음처럼 함수의 매개변수로 들어가서 사용된다. 

이제는 대부분의 언어에서 상식이 되버린 map, filter 함수에서도 사용된다. 


scala> numbers.map((i: Int) => i * 2)
res0: List[Int] = List(2, 4, 6, 8, 10)
scala> numbers.filter((i: Int) => i % 2 == 0)
res0: List[Int] = List(2, 4)


간단한 형태의 함수 리터럴 

scala> numbers.filter( i => i % 2 == 0)
res0: List[Int] = List(2, 4)


애초에 컬렉션에서 i 는 정수라는 하실을 알기 때문에 굳이 Int 라고 하지 않아도 된다.

타깃 타이핑이라고 하며 () 도 생략 가능하다. 


더 간단한 형태의 함수 리터럴 

scala> numbers.filter( _ > 4)
res0: List[Int] = List(5)

_ 는 함수가 호출 시 전달받은 인자로 하나씩 채워져서 계산될 것이다.  




기타 특성  

반복 파라미터 

def echo (args: String*) = for (arg <-args) println(arg)    //   매개변수에 * 를 표시함으로써 

echo("hello", "world")  // 요렇게 인자의 갯수를 자유롭게 할 수 있다. 


이름 있는 인자 

def speed(distance: Float, time: Flot) : Flot = distance / time 

speed(100,10) // 일반적 사용

speed(time = 10, distance = 100) // 이름을 함께 사용, 순서를 바꾸어서 사용해도 된다.


디폴트 값

def printTime(out: java.io.PrintStream = Console.out) = out.println("time = " + System.currentTimeMillis()) 

printTime() //  이렇게 호출하면 자동적으로 디폴트로 out 은 Console.out 이 된다. 


클로저

 ( 나중에 따로 다루겠습니다 )




def 와 val 의 차이 

def even: Int => Boolean = _ % 2 == 0
val even: Int => Boolean = _ % 2 == 0

even(10) // 둘다 이렇게 호출 가능


위에서 배운 바와 같이 함수는 이렇게 표현 가능한데요. 둘의 차이는 멀까요? 


메소드 def even 은 호출 되었을때 평가되며 새로운 Function1 객체를 매 호출시마다 생성합니다.

반면 val 은 정의 되었을때 평가됩니다. 

val test: () => Int = {  // 이미 평가됨  (리턴값이 항상 같을 경우 사용 할 수 있다) 
  val r = util.Random.nextInt
  () => r
}

test()
// Int = -1049057402
test()
// Int = -1049057402 - 동일한 값 

def test: () => Int = {   // 호출할때마다 평가가 달라짐 
  val r = util.Random.nextInt
  () => r
}

test()
// Int = -240885810
test()
// Int = -1002157461 - 값이 달라 졌다. 

* lazy val 로 선언하면 이름에서 느껴지는것 처럼 정의 되었을대 평가되는게 아니라 처음 호출시 평가됩니다.


스칼라에서의 함수 호출


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


def formatEuro (amt : Double) = {.......}   <-- 와 같은 Double 형의 매개변수 하나를 받는 함수가 있을 시

formatEuro { val rate = 1.32; 0.235 + 0.7123 + rate * 5.32 }   <-- 이렇게 호출 할 수 있는데, {} 블럭 안에서 평가된 rate 가 매개변수로 들어가게 된다.


더 간단하게 예를 들면


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


스칼라에서 이렇게 한 이유는 프로그래머가 중괄호{} 내부에서 함수 리터럴을 직접 사용하도록 하기 위해서인데, 

이렇게 작성한 메소드는 호출 시 좀 더 제어 추상화 구문과 비슷해진다. 좀 더 수준높은 예제를 들어 보면 


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


val writer = new PrintWriter(file)

try {

    op (writer)

} finally {

    writer.close()

}


}


이런 함수를 호출 할 때는 


withPrintWriter ( new File ("date.txt") , writer => writer.println(new java.util.Date) ) 


이렇게 호출 하며, 여기서는 인자가 2개이기 때문에 { } 를 사용할 수 없다. 하지만 !!!!


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


}


이러한 함수라면 {} 를 사용 할 수 있다.


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


}


(curry vs partially applying 이라는 헥깔림 주제도 있다)





웹 MVC 컨트롤러에서의 예  (Play for Scala)  


스프링에서 컨트롤러 함수의 예가  다음과 같은 모습을 취하는 반면 

 @RequestMapping(method = RequestMethod.GET)
   public String printHello(ModelMap model) {
      model.addAttribute("message", "Hello Spring MVC Framework!");
      return "hello";
   }

스칼라언어 기반 Play 프레임워크 웹개발에서는 (위의 자바 예와 동일한 내용의 예가 아닙니다) 

def doSomething = Action{
                          Ok.apply(views.html.index("Hi there"))
                  }

이런 모양새로 이루어지는데 여기서 Action 이 무엇일까요? 


위의 예는 다음과 같이 풀어지게 되는데요

def doSomething: Action[AnyContent] = Action.apply({
  Ok.apply(views.html.index("Hi there"))
})

위의 apply 메소드의 정의는  apply(block: => Result) 이라 익명의 함수가 들어가게 됩니다.  

Action.apply 함수의 () 는 생략가능이고 apply 함수내에는 인자가 없고 Result 를 리턴하는 함수가 들어가네요.
이 함수 말이죠. 

{
  Ok.apply(views.html.index("Hi there"))
}

그리고 doSomething 메소드는  Action[AnyContent]  클래스의 인스턴스를 리턴합니다.

항상 같은 값을 리턴하기때문에 def 을 빼도 됩니다.  즉 메소드가 아니라 val 이어도 된다는 말.


Action { request =>
  Ok("Got request [" + request + "]")
}

처럼 인자가 있는 함수가 들어가기도 합니다.

0 Comments
댓글쓰기 폼