F# Nullable Operators

F# Nullable Operators

DB처럼 null을 값 대신 허용하는 데이터 소스를 다루다 보면, 일단 "비어 있을 수도 있는" 숫자·비교값을 만나게 돼요. F#의 nullable operators는 이런 nullable 산술 타입을 한쪽이나 양쪽에 두고 쓸 수 있게 해 주는 이항 산술·비교 연산자예요. 주로 쿼리 식 안에서 쓰이고요, 산술·비교 연산자 외에도 nullable 타입끼리 변환하는 변환 연산자, 그리고 일부 쿼리 연산자의 nullable 버전도 있어요. 이 글에서는 그 목록과 사용법을 차근차근 정리해 볼게요.

출처: Nullable Operators in Queries — Microsoft Learn (F#)

본문

쿼리 식 밖에서는 사실 잘 만날 일이 없어요. nullable 연산자는 대체로 쿼리 식 안에서만 쓰이거든요. 만약 쿼리 식을 쓰지 않는다면 이 연산자들을 굳이 알 필요는 없어요. 다만 데이터베이스 등에서 null을 허용하는 컬럼을 가져와서 조건을 걸어야 한다면, 이 연산자들이 큰 도움이 돼요.

Nullable 연산자 표

F#에서 지원하는 nullable 연산자를 정리하면 다음과 같아요. 왼쪽이 nullable일 때, 오른쪽이 nullable일 때, 그리고 양쪽 다 nullable일 때 세 가지 변형이 각각 존재해요.

왼쪽 nullable 오른쪽 nullable 양쪽 nullable
?>= >=? ?>=?
?> >? ?>?
?<= <=? ?<=?
?< <? ?<?
?= =? ?=?
?<> <>? ?<>?
?+ +? ?+?
?- -? ?-?
?* *? ?*?
?/ /? ?/?
?% %? ?%?

흥미로운 점은 각 이항 연산자마다 "물음표 위치"만 바꿔서 방향을 표현한다는 거예요. 예를 들어 ?> 는 "왼쪽 값이 nullable이고 오른쪽과 비교"한다는 뜻이고, ?>? 는 "양쪽 다 nullable일 때 비교"한다는 뜻이에요. 패턴이 좀 익숙해지면 이름만 봐도 의미가 바로 떠올라요.

Remarks

nullable 연산자들은 FSharp.Linq 네임스페이스 안의 NullableOperators 모듈에 포함되어 있어요. 그리고 nullable 데이터의 타입은 System.Nullable<'T> 로 표현돼요.

쿼리 식에서 nullable 타입은 "값 대신 null을 허용하는 데이터 소스"에서 데이터를 선택할 때 생겨요. 예를 들어 SQL Server 데이터베이스에서 테이블의 각 컬럼에는 null 허용 여부를 나타내는 속성이 있어요. null이 허용되면, 데이터베이스에서 돌아오는 데이터에는 intfloat 같은 원시 타입으로는 표현할 수 없는 null이 섞여 있을 수 있어요. 그래서 그 데이터는 int 대신 System.Nullable<int> 로, float 대신 System.Nullable<float> 로 반환되는 거예요.

System.Nullable<'T> 객체에서 실제 값을 꺼낼 때는 Value 속성을 쓰고, 값이 있는지 없는지는 HasValue 메서드로 확인해요. 유용한 메서드가 하나 더 있는데, 바로 System.Nullable<'T>.GetValueOrDefault 예요. 이건 값이 있으면 그 값을, 없으면 해당 타입의 기본 값을 돌려줘요. 여기서 기본 값이란 "0"에 해당하는 값, 즉 0, 0.0, false 같은 걸 말해요.

nullable 타입은 intfloat 같은 일반 변환 연산자를 써서 non-nullable 원시 타입으로 바꿀 수도 있어요. 또 nullable 타입에서 다른 nullable 타입으로 변환할 수도 있는데, 이때는 nullable 타입용 변환 연산자를 사용해요. 이 연산자들은 기본 연산자와 이름이 같지만, FSharp.Linq 네임스페이스 안의 별도 모듈인 Nullable 모듈에 들어 있어요. 쿼리 식을 다룰 때 보통 이 네임스페이스를 열어두게 되는데, 그러면 아래 코드처럼 해당 변환 연산자 앞에 Nullable. 접두사를 붙여서 쓸 수 있어요.

open Microsoft.FSharp.Linq
let nullableInt = new System.Nullable<int>(10)
// Use the Nullable.float conversion operator to convert from one nullable type to another nullable type.
let nullableFloat = Nullable.float nullableInt
// Use the regular non-nullable float operator to convert to a non-nullable float.
printfn $"%f{float nullableFloat}"

실행 결과는 10.000000 이에요.

nullable 데이터 필드를 위한 쿼리 연산자, 예를 들어 sumByNullable 같은 것도 쿼리 식에서 쓸 수 있도록 준비되어 있어요. 여기서 주의할 점이 하나 있어요. non-nullable 타입용 쿼리 연산자는 nullable 타입과 타입 호환이 안 되기 때문에, nullable 데이터 값을 다룰 때는 반드시 그에 맞는 nullable 버전의 쿼리 연산자를 써야 해요. 자세한 내용은 Query Expressions를 참고해요.

다음 예제는 F# 쿼리 식에서 nullable 연산자를 어떻게 쓰는지 보여줘요. 첫 번째 쿼리는 nullable 연산자 없이 작성한 모습이고, 두 번째 쿼리는 같은 동작을 nullable 연산자를 써서 표현한 모습이에요. 데이터베이스를 세팅하는 전체 맥락을 보고 싶다면 Walkthrough: Accessing a SQL Database by Using Type Providers를 참고해요.

open System
open System.Data
open System.Data.Linq
open Microsoft.FSharp.Data.TypeProviders
open Microsoft.FSharp.Linq
[<Generate>]
type dbSchema = SqlDataConnection<"Data Source=MYSERVER\\INSTANCE;Initial Catalog=MyDatabase;Integrated Security=SSPI;">
let db = dbSchema.GetDataContext()
query {
    for row in db.Table2 do
    where (row.TestData1.HasValue && row.TestData1.Value > 2)
    select row
} |> Seq.iter (fun row -> printfn $"%d{row.TestData1.Value} %s{row.Name}")
query {
    for row in db.Table2 do
    // Use a nullable operator ?>
    where (row.TestData1 ?> 2)
    select row
} |> Seq.iter (fun row -> printfn "%d{row.TestData1.GetValueOrDefault()} %s{row.Name}")

첫 쿼리에서는 HasValue로 값이 있는지 먼저 확인하고 Value로 실제 값을 꺼내 비교했죠. 두 번째 쿼리는 ?> 연산자 하나로 "nullable 값을 2와 비교"한다는 걸 간결하게 표현했어요. 같은 일을 하는 코드지만, nullable 연산자를 쓰면 훨씬 읽기 편해지는 걸 느낄 수 있어요.

더 알아보기