ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [프언] #02. Programming in Scala
    2023-2학기/프로그래밍언어 2023. 9. 9. 03:53

    Scala란?

    이 수업에서는 기본 언어로 스칼라를 사용합니다. 스칼라라는 언어가 생소하신 분들이 많을겁니다. 저도 그랬고요. 그럼에도 스칼라를 사용하는 이유는 이 언어가 현대적인 언어의 특징을 많이 가지고 있기 때문입니다. 자세한 내용은 스칼라를 공부할수록 더 자세하게 아실 수 있을 겁니다.

    윈도우용 Scala Installer

    저는 스칼라를 이 과목을 공부하며 처음 사용한거라 설치를 해주었는데요, 이 링크에 들어가면 위 사진처럼 윈도우용 설치기가 있어 저는 그걸 사용했습니다. 간단하게 사용하실 분들은 Scalafiddle이나 Scastie 등의 온라인 툴을 사용하셔도 됩니다.

    스칼라는 기본적으로 JVM(Java Virtual Machine, 자바를 실행할 수 있는 환경을 만들어주는 가상 머신) 위에서 실행됩니다. 스칼라로 짜여진 프로그램은 Scala Compiler에 의해 Java Bytecode로 변환되고, 이를 실행할 수 있는 자바 환경이 필요하기 때문입니다.

    다음으로 설치해준 것은 SBT라는 프로젝트 관리 툴인데요, 저는 이 링크에서 설치했습니다.

    명령 프롬프트에서 sbt를 실행한 모습.

    설치 후에 명령 프롬프트에서 sbt를 입력하시면 sbt가 실행됩니다. 이제 본격적으로 스칼라라는 언어에 대해 알아보도록 하겠습니다. 저도 스칼라를 이 수업을 들으면서 처음 배웠기 때문에 스칼라 기본 문법에 대해서 글에 적을 예정이지만, 프로그래밍 언어 자체를 처음 접하는 사람들이 아닌 C, Python등 다른 프로그래밍 언어에 익숙하신 분들을 대상으로 작성하겠습니다.

     

    Expression & Statement

    스칼라라는 언어를 공부할 때 가장 기본이 되는 단위는 바로 Expression입니다. 이는 Evaluate 될 수 있는, 즉 계산 가능한 코드의 일부분을 의미합니다. (스칼라에서만 쓰는 용어는 아니고 프로그래밍 언어 전반적으로 사용되는 용어입니다.) 예를 들어, 아래의 코드들은 전부 Expression입니다.

    1 + 1
    "Hello " + "World"

    첫번째 코드는 계산하면 2라는 Int가 되고, 두번째 코드는 계산하면 "Hello World" 라는 String이 됩니다. 이렇게 모든 Expression은 그 계산값을 가집니다. 이것이 Expression의 가장 큰 특징입니다. Expression과 비슷하면서 다른 개념이 Statement인데요, Statement는 실행 가능한 코드의 일부분을 부르는 말입니다.

    if x == 1:
    	x = 50
    else: x = 10

    이건 스칼라 코드는 아니고 Python 코드인데요, 이 코드는 실행은 가능하지만 이 코드 자체가 어떤 값을 가지진 않습니다. 이런 코드를 Statement라고 합니다.

    C, Python 등 많은 프로그램 언어들이 Statement를 기반으로 이루어져 있습니다. 그러나 스칼라는 Expression-Oriented Language로, 아주 많은 부분들이 Expression으로 구성되어 있다는 점 참고하시면 좋을 듯 합니다.

    Expression들은 아래와 같이 println을 이용해 콘솔에 출력할 수 있습니다.

    println(1 + 1)
    println("Hello " + "World")

     

    Variable

    스칼라에서도 Variable(변수)을 지정하고, 그 변수를 나중에 가져다가 쓸 수 있습니다. 아래 예시를 봅시다.

    val x = 10 + 2
    println(x)

    이렇게 10 + 2라는 Expression의 계산값에 x라는 이름을 붙여줄 수 있습니다. 그리고 x 그 자체도 Expression이기 때문에, println을 이용하여 콘솔에 출력할 수 있습니다. 주의점은, 위와 같이 지정된 변수들은 값을 수정할 수 없습니다. 

    val x = 10 + 2
    x = 3

    즉, 이런 코드는 에러가 발생합니다.

    변수를 정의할 때, "val <name>[: type] = <expression>"과 같이 입력하여 변수의 타입을 지정해서 변수를 정의할 수 있습니다. 아래의 예시는 10 + 2를 x라고 정의할 때 x의 타입을 Int로 지정해준 것입니다.

    val x: Int = 10 + 2

    이렇게 알아봤던 변수들은 수정할 수 없는 변수, 즉 Immutable Variable이라 불립니다. 수정 가능한 변수를 만들기 위해서는 아래와 같이 val 대신 var을 사용합니다.

    var x: Int = 10
    x = 5

    이 코드는 에러가 발생하지 않습니다.

     

    Built-in Types & Expressions

    스칼라 언어에서 기본적으로 사용할 수 있는 변수의 타입은 C, Python 같은 언어들과 크게 다르지 않습니다. 숫자를 나타낼 수 있는 타입은 아래와 같습니다.

    • Byte (8bits)
    • Short (16bits)
    • Int (32bits)
    • Long (64bits)
    • Float (32bits)
    • Double (32bits)

    문자를 표현하기 위한 Char (문자 1개), String (문자열) 타입도 있으며, true/false 값만을 가지는 Boolean 타입도 사용할 수 있습니다.

    아래와 같이 사칙연산과 Boolean 타입간의 and, or 등의 연산기호도 사용할 수 있습니다.

    val add = 1 + 1
    val sub = 2 - 1
    val mlu = 3 * 4
    val div = 10 / 2
    println(s"${add} ${sub} ${mul} ${div}")
    // prints 2 1 12 5
    
    val t = true && true // true and true
    val f = true || false // true or false
    println(s"${t} ${f}")
    // prints true true

    println에 새로운 문법이 나왔는데, 그냥 간단하게 여러가지 값들을 출력하는 방법으로 생각하면 됩니다. 자세하게 알고 싶으신 분은 이 링크에 들어가서 설명을 읽어보시면 될 듯 합니다.

     

    Block

    Block은 여러 Expression을 합쳐서 하나의 Expression으로 합쳐주는 역할을 합니다. 하나의 Block은 하나의 Expression이며, 시작과 끝은 {}로 표시합니다.

    val y = {
    	val x = 2 + 3
    	x + 4
    }
    println(y)
    // prints 9

    아까 Block은 Expression이라고 했죠? Expression은 그 정의에 따라 Evaluate 가능하기 때문에, Block 역시 Evaluate 할 수 있고 그 결과값도 존재합니다. Block을 Evaluate한 값은 Block의 가장 마지막 줄이 되는데요, 위의 예시에서 Block 내부에서 2 + 3이 x로 정의되었고 마지막 줄의 x + 4가 이 Block의 결과값이 되므로 이 Block을 Evaluate하면 9가 됩니다.

     

    First-Class Function and Method

    스칼라에서는 함수 역시 Expression입니다. 이 말은 즉, 함수가 하나의 value에 할당될 수 있다는 뜻이겠죠? 아래의 예시를 보겠습니다.

    val addOne = (x: Inx) => x + 1
    println(addOne(1))

    이렇게 "(<name>[: type], <name>[:type], ...) => <expression>"과 같이 함수를 정의할 수 있습니다. 이렇게 변수에 지정해서 만들어진 함수를 First-Class Function, 혹은 Anonymous Function이라고 부릅니다. 

    반면 C, C++과 유사하게 함수를 정의할 때 value에 할당하지 않고 함수를 정의할 수도 있습니다.

    def add(x: Int, y: Int): Int = x + y
    println(add(1, 2))

    이렇게 def 키워드를 통해 "def <name>(<arg_0>[: type], <arg_1>[:type], ...) = <expression>"과 같이 정의된 함수를 Method라고 합니다. 보통 Function이라고 말하면 위에 있는 First-Class Function이 아닌 Method를 부르는 것입니다.

     

    Class

    스칼라에서도 다른 객체 지향 언어와 같이 Class를 정의할 수 있습니다. 기본 문법은 "class <name>(<name>[: type], <name>[: type], ...)"입니다.

    class Greeter(prefix: String, suffix: String) {
    	def greet(name: String): Unit = 
        	println(prefix + name + suffix)
    }

    이 Class의 변수는 String인 prefix와 suffix이고, greet라는 함수를 가지네요. 객체를 만들기 위해서는 new 키워드를 이용해 아래와 같이 적으면 됩니다.

    val greeter = new Greeter("Hello, ", "!")
    greeter.greet("AlriC")
    // prints Hello, AlriC!

    또한, 스칼라는 Case Class라는 또 다른 타입의 클래스를 지원합니다. Case Class를 정의할 때는 case 키워드를 붙여줍니다.

    case class Point(x: Int, y: Int)
    val point = Point(1, 2)

    이렇게 예시로 좌표평면 위의 한 점을 나타내는 Case Class를 하나 만들어봤습니다. 코드의 2번째 줄을 보면 일반 Class의 객체를 만들 때와는 다르게 new 키워드를 사용하지 않았죠? Case Class의 객체를 만들 때는 new 키워드 대신 일반 expression과 같이 val 키워드를 사용하여 객체를 만듭니다.

    Case Class의 가장 큰 특징으로는, 2개의 객체를 비교할 때 객체 그 자체가 아닌 변수에 의해 비교를 한다는 것입니다. 말로는 살짝 이해가 안되실 수 있는데 아래의 예시를 보면 좀 더 쉽게 와닿을 듯 하네요.

    class Point_class(x: Int, y: Int)
    new P = Point_class(1, 2)
    new Q = Point_class(1, 2)
    println(P == Q)
    // print false
    
    case class Point_case_class(x: Int, y: Int)
    val R = Point_case_class(1, 2)
    val S = Point_case_class(1, 2)
    println(R == S)
    // print true

    위쪽 부분에서는 일반 class인 Point_class를 정의하고 2개의 객체 P, Q를 만들었습니다. 이 때 P와 Q는 같은 변수를 가지고 있기는 하지만 엄연히 다른 객체이기 때문에 메모리 상에서 다른 위치에 저장되어 있습니다. 그래서 P == Q의 계산값은 false입니다.

    반면 아래쪽 부분에서는 Case Class인 Point_case_class를 정의하고 똑같이 R, S를 만들어주었는데요, R과 S는 다른 객체이기는 하지만 둘은 Case Class이기 때문에 R == S를 계산할 때 R과 S의 변수를 비교합니다. R과 S는 1, 2로 같은 변수를 가지고 있기 때문에 예시 코드의 마지막 줄은 true를 출력합니다.

    또, Object로 정의할 수 있는 또 다른 종류의 클래스도 존재합니다. 이 클래스의 가장 큰 특징은 바로 singleton이라는 것입니다. 방금 위에 설명한 예시에서는 Point라는 클래스를 만들고 그 클래스로 여러개의 객체를 만들 수 있었잖아요? Object는 클래스 하나 당 객체가 하나씩만 존재합니다. 아래는 그 예시입니다.

    object IdFactory {
    	private var counter = 0
    	def create(): Int = {
    		counter += 1
    		counter
    	}
    }
    
    val newId: Int = IdFactory.create()
    println(newId)
    // prints 1
    
    val newerId: Int = IdFactory.create()
    println(newrId)
    // prints 2

    IdFactory는 하나의 변수 counter를 가지고, 이 값은 객체를 처음 만들때 0으로 초기화됩니다. create 함수는 원래의 counter 값에 1을 더한 후 그 값을 갖는 함수입니다. (이 함수 역시 Expression이기 때문에 리턴한다는 표현을 쓰지 않았습니다.)

    IdFactory 클래스는 Object로 정의되었기 때문에 하나의 객체만을 가집니다. 처음으로 newId라는 객체를 만들면 객체의 counter 값이 0으로 지정되고, create 함수를 바로 뒤에 실행하였으니 counter 값은 1이 됩니다. 그 뒤에 newerId라는 이름의 새로운 객체를 만들었지만, 새롭게 정의된 객체에서 create 함수를 호출해도 newerId라는 새로운 객체의 새로운 counter 값에 접근하는 것이 아니라, 아까 연산했던 counter 값에 접근하여 1이 더해지기 때문에 마지막 줄은 2를 출력하게 됩니다.

     

    Conditional Expression (IF)

    많은 프로그래밍 언어에서 if문을 사용할 때, if문은 Expression이 아니라 Statement입니다. 

    if x == 1:
    	x = 50
    else: x = 10

    저 위에서 Expression과 Statement의 차이를 설명할 때 들었던 Python 코드의 예시인데요, 이렇게 Statement 형태로 구성된 if문이 아마 많은 분들에게 익숙하실 겁니다. 그러나, Scala에서는 if문조차 Expression입니다. 예시를 한번 보겠습니다.

    val x = if (x == 1) 50 else 10
    println(x)

    x의 값이 1이면 50, 1이 아니면 10이 x에 저장되는 코드입니다. 이렇게 Scala에서 if문은 "if (<expression>) <expression> else <expression>"으로 이것 자체가 Expression입니다.

    if (1 == 1) 50 else 10

    이렇게 if문만 떼놓고 보아도 이 if문 자체가 50이라는 값을 가집니다. Scala에서는 if문이 Evaluate 가능한 Expression이기 때문이죠. if문을 활용한 예시 하나만 더 보겠습니다.

    val factorial: Int => Int = (x: Int) => {
    	if (x == 1) x
    	else x * factorial(x - 1)
    }
    println(factorial(6))
    // prints 720

    이 함수 factorial은 Int를 하나 받아서 Int를 출력하는 함수입니다. 입력한 Int가 1일 경우 그 Int를 그대로 함수값으로 갖고, 입력한 Int가 1이 아닐 경우 x - 1의 함숫값과 x를 곱해서 함숫값으로 갖는, Recursive하게 정의된 함수입니다. 다들 눈치를 채셨겠지만 이 함수는 팩토리얼을 계산하는 함수입니다.

     

    List & Tuple & Map

    Scala에서도 역시 List 자료형을 제공합니다.

    val empty = List()
    val two = List[Int](1, 2)

    그리고 이 List는 Immutable, 수정할 수 없으며 한 List에 있는 모든 원소는 전부 같은 Type이여야 합니다. List(1, "a")와 같은 정의는 불가능합니다.

    List와 유사한 자료형 중 Tuple이라는 자료형도 존재합니다. Tuple 역시 수정할 수 없는 특성을 가지지만, Tuple은 모든 원소가 전부 같은 Type일 필요가 없습니다. Tuple의 자료형에 접근하기 위해서는 "tuple._<integer>"과 같이 쓰시면 됩니다.

    val tuple = ("number", 1)
    val first = tuple._1	// "number"
    val second = tuple._2	// 1

    다음 자료형은 Map입니다. Map은 key와 value로 이루어진 한 쌍의 집합입니다. 역시 Immutable입니다.

    val a = Map(1 -> "John", 2 -> "Mary")

    이렇게 만들어진 Map은 Key가 1이고 Value가 "John"인 값 하나와 Key가 2이고 Value가 "Mary"인 값 하나로 이루어져 있습니다. Map에 있는 값에 접근할 때는 아래와 같이 하시면 됩니다.

    val m = map(1 -> 2)
    val res = m(1)

     

    Package

    다른 프로그래밍 언어에서 변수, 함수들의 명칭이 소속된 공간을 Namespace라고 부릅니다. Scala에서 이 Namespace 개념을 담당하고 있는 것은 바로 Package 입니다. 새로운 Package는 아래와 같이 생성할 수 있습니다.

    package example

    이렇게 example 이라는 package를 선언하면 이 파일의 나머지 코드들은 전부 example 안에 존재하게 됩니다. 한 파일에서 다른 namespace를 불러오기 위해서는 import 인자를 사용합니다.

    감사합니다.


    이 글은 컴퓨터공학과 학부생이 개인 공부 목적으로 작성한 글이므로, 부정확하거나 틀린 내용이 있을 수 있으니 참고용으로만 봐주시면 좋겠습니다. 레퍼런스 및 글에 대한 기본적인 정보는 이 글을 참고해 주세요.

    '2023-2학기 > 프로그래밍언어' 카테고리의 다른 글

    [프언] #05. Expressions  (0) 2023.09.25
    [프언] #04. Functional Programming  (2) 2023.09.24
    [프언] #03. Inductive Definitions  (0) 2023.09.09
    [프언] #01. Introduction  (0) 2023.09.02
    [프언] #00. Course Information  (0) 2023.08.31
Designed by Tistory.