Import 선언: open 키워드

Import 선언: open 키워드

import 선언은 특정 모듈이나 네임스페이스의 요소들을 정규화된(fully qualified) 이름을 붙이지 않고 바로 참조할 수 있게 해 줘요. F# 코드를 쓸 때 반복해서 전체 경로를 쓰는 일을 피하고 싶다면, 이 open 키워드가 아주 자주 찾게 되는 도구예요. C#의 using, Visual C++의 using namespace, Visual Basic의 Imports와 비슷한 역할을 한다고 보면 돼요.

출처: Microsoft Learn - Import declarations: The open keyword (2021-11-05 기준)

본문

사용법과 이유

매번 전체 네임스페이스나 모듈 경로를 붙여서 코드를 참조하다 보면, 쓰기도 어렵고 읽기도 어렵고 유지보수도 힘들어져요. 대신 자주 쓰는 모듈과 네임스페이스에는 open 키워드를 써서, 그 모듈이나 네임스페이스의 멤버를 참조할 때 짧은 형태의 이름만 사용하면 돼요.

open으로 지정하는 모듈이나 네임스페이스는 같은 프로젝트 안에 있거나, 참조된 프로젝트나 어셈블리 안에 있어야 해요. 그렇지 않다면 프로젝트에 참조를 추가하거나, -reference 명령줄 옵션(줄여서 -r)을 쓰면 돼요. 자세한 내용은 Compiler Options를 참고해요.

import 선언이 지정하는 이름들은 그 선언 뒤에 따라오는 코드부터, 그 코드를 감싸고 있는 네임스페이스·모듈·파일의 끝까지 사용할 수 있어요.

import 선언을 여러 개 쓸 때는 각각 서로 다른 줄에 배치해야 해요.

다음 코드는 open 키워드가 어떻게 코드를 간결하게 만드는지 보여 줘요.

// Without the import declaration, you must include the full
// path to .NET Framework namespaces such as System.IO.
let writeToFile1 filename (text: string) =
  let stream1 = new System.IO.FileStream(filename, System.IO.FileMode.Create)
  let writer = new System.IO.StreamWriter(stream1)
  writer.WriteLine(text)

// Open a .NET Framework namespace.
open System.IO

// Now you do not have to include the full paths.
let writeToFile2 filename (text: string) =
  let stream1 = new FileStream(filename, FileMode.Create)
  let writer = new StreamWriter(stream1)
  writer.WriteLine(text)

writeToFile2 "file1.txt" "Testing..."

보시다시피 첫 번째 함수에서는 System.IO.FileStream, System.IO.FileMode처럼 전체 경로를 모두 붙였는데, open System.IO 뒤에 정의한 두 번째 함수에서는 FileStream, FileMode처럼 짧은 이름만 쓰면 돼요.

이름 충돌(모호성) 주의

F# 컴파일러는 둘 이상의 open 모듈이나 네임스페이스에 같은 이름이 있을 때 오류나 경고를 뱉지 않아요. 대신 모호함이 생기면 더 나중에 open된 모듈이나 네임스페이스를 우선해요. 예를 들어 다음 코드에서 emptyListSeq 모듈 둘 다에 있지만, 결과적으로 Seq.empty를 의미해요.

open List
open Seq
printfn %"{empty}"

그래서 ListSeq처럼 동일한 이름의 멤버를 가진 모듈이나 네임스페이스를 open할 때는 조심해야 해요. 그런 경우 정규화된 이름을 쓰는 걸 고려하고, 코드가 import 선언의 순서에 의존하게 되는 상황은 피하는 게 좋아요.

Open type 선언

F#은 타입에도 open을 쓸 수 있어요. 예를 들어 이렇게요.

open type System.Math
PI

이렇게 하면 해당 타입의 접근 가능한 모든 static 필드와 멤버가 노출돼요.

또한 F#으로 정의한 레코드와 판별 공용체(discriminated union) 타입을 열어서 static 멤버를 노출시킬 수도 있어요. 판별 공용체의 경우에는 union case도 노출할 수 있어요. 모듈 안에 선언된 타입의 union case에 접근하고 싶은데 그 모듈 전체는 열고 싶지 않을 때 특히 유용해요. 예를 들어 이렇게요.

module M =
    type DU = A | B | C

    let someOtherFunction x = x + 1

// Open only the type inside the module
open type M.DU

printfn "%A" A

루트 경로에서만, global 지정자로 열기

중첩된 모듈, 예를 들어

module A =
    module B =
        ...

는 이렇게 열 수 있어요.

open A // opens A
open B // opens A.B

완전히 정규화된(fully qualified) 모듈이나 네임스페이스만 열고 싶다면 global 지정자를 붙이면 돼요.

open global.A   // works
open global.B   // this now fails
open global.A.B // works

기본으로 열려 있는 네임스페이스

일부 네임스페이스는 F# 코드에서 너무 자주 쓰여서, 명시적인 import 선언 없이도 암시적으로 열려 있어요. 다음 표는 기본으로 열려 있는 네임스페이스들이에요.

네임스페이스 설명
FSharp.Core int, float 같은 기본 제공 타입에 대한 F# 타입 정의를 담고 있어요.
FSharp.Core.Operators +, * 같은 기본 산술 연산을 담고 있어요.
FSharp.Collections List, Array 같은 불변 컬렉션 클래스를 담고 있어요.
FSharp.Control 지연 평가(lazy evaluation)와 async 표현식 같은 제어 구문용 타입을 담고 있어요.
FSharp.Text printf 함수처럼 서식을 갖춘 입출력을 위한 함수를 담고 있어요.

AutoOpen 특성

어셈블리에 AutoOpen 특성을 적용하면, 그 어셈블리가 참조될 때 네임스페이스나 모듈을 자동으로 열 수 있어요. 모듈에 AutoOpen 특성을 적용하면, 부모 모듈이나 네임스페이스가 열릴 때 그 모듈도 자동으로 열려요. 자세한 내용은 AutoOpenAttribute를 참고해요.

RequireQualifiedAccess 특성

일부 모듈, 레코드, 공용체 타입은 RequireQualifiedAccess 특성을 지정할 수 있어요. 그런 모듈·레코드·공용체의 요소를 참조할 때는 import 선언을 포함하더라도 반드시 정규화된 이름을 써야 해요. 흔히 쓰이는 이름을 정의하는 타입에 이 특성을 전략적으로 사용하면 이름 충돌(name collision)을 피할 수 있고, 그래서 라이브러리가 변해도 코드가 더 튼튼해져요. 자세한 내용은 RequireQualifiedAccessAttribute를 참고해요.

더 알아보기