측정 단위
측정 단위 (Units of Measure)
F#에서 부동소수점 값과 부호 있는 정수 값에는 단위(Units of Measure)를 붙일 수 있어요. 주로 길이, 부피, 질량 같은 물리량을 나타낼 때 사용하죠. 단위가 붙은 양을 사용하면 컴파일러가 산술식의 단위가 서로 맞는지 검증해 줘서, 프로그램 오류를 예방하는 데 큰 도움이 돼요.
출처
본문
참고: 이 예시들은 단위 측정을 포함하는 산술 계산의 올바름을 보여주는 자료예요. 이 기능은 또한 FSharp.UMX 프로젝트처럼, 표현 비용이 전혀 들지 않는 타입 안전한 주석을 다른 타입에 추가하는 용도로도 쓸 수 있어요.
구문
[<Measure>] type unit-name [ = measure ]
설명 (Remarks)
위 구문은 unit-name을 측정 단위로 정의해요. 선택 항목(option part)은 이전에 정의된 단위를 바탕으로 새 단위를 정의할 때 사용해요. 예를 들어 다음 한 줄은 cm(센티미터) 단위를 정의해요.
[<Measure>] type cm
다음 한 줄은 ml(밀리리터)를 세제곱센티미터(cm^3)로 정의해요.
[<Measure>] type ml = cm^3
위 구문에서 measure는 단위를 포함하는 수식(공식)이에요. 단위를 포함한 수식에서는 정수 거듭제곱(양수·음수)이 지원돼요. 단위 사이의 공백은 두 단위의 곱을 나타내고, *도 단위의 곱을, /는 단위의 몫(비)을 나타내요. 역수 단위는 음의 정수 거듭제곱을 쓰거나, 단위 수식의 분자와 분모를 구분해 주는 /를 쓰면 돼요. 분모에 여러 단위가 오면 괄호로 감싸 주어야 해요. / 뒤에 공백으로 구분된 단위들은 분모에 속하는 것으로 해석되지만, * 뒤에 오는 단위들은 분자에 속하는 것으로 해석돼요.
단위 수식에서 1을 단독으로 사용해 무차원(dimensionless) 양을 나타내거나, 다른 단위와 함께(예를 들어 분자 위치에) 사용할 수 있어요. 예를 들어 비율(rate)의 단위는 1/s처럼 쓰는데, 여기서 s는 초(seconds)를 뜻해요. 단위 수식에는 괄호를 쓰지 않아요. 단위 수식 안에 숫자 변환 상수를 직접 지정하지는 않지만, 별도로 단위가 붙은 변환 상수를 정의해서 단위 검사가 되는 계산에 사용할 수는 있어요.
의미가 같은 단위 수식은 여러 가지 동등한 방식으로 쓸 수 있어요. 그래서 컴파일러는 단위 수식을 일관된 형태로 변환해요. 이 과정에서 음의 거듭제곱은 역수로 바꾸고, 단위를 하나의 분자와 분모로 묶으며, 분자·분모 안의 단위들을 알파벳순으로 정렬해요.
예를 들어 단위 수식 kg m s^-2와 m /s s * kg는 모두 kg m/s^2로 변환돼요.
부동소수점 수식에서 단위를 사용할 수 있어요. 단위가 붙은 부동소수점 수를 쓰면 타입 안전성 차원이 하나 더 추가돼서, 약한 타입의 부동소수점 수를 쓸 때 수식에서 생길 수 있는 단위 불일치 오류를 피하는 데 도움이 돼요. 단위를 쓰는 부동소수점 수식을 작성한다면, 그 수식 안의 단위들은 서로 맞아야 해요.
리터럴에 꺾쇠괄호(angle brackets)로 단위 수식을 붙여 주석을 달 수 있어요. 다음 예시를 확인해 볼게요.
1.0<cm>
55.0<miles/hour>
숫자와 꺾쇠괄호 사이에는 공백을 넣지 않아요. 다만 다음 예시처럼 f 같은 리터럴 접미사를 붙일 수는 있어요.
// The f indicates single-precision floating point.
55.0f<miles/hour>
이런 주석은 리터럴의 타입을 원시 타입(예: float)에서 차원이 붙은 타입으로 바꿔요. 예를 들면 float<cm>이나, 이 경우에는 float<miles/hour>가 되죠. <1> 단위 주석은 무차원 양을 나타내며, 그 타입은 단위 매개변수가 없는 원시 타입과 동등해요.
측정 단위의 타입은 부동소수점 또는 부호 있는 정수 타입에 꺾쇠괄호로 표시한 단위 주석이 추가된 형태예요. 그래서 g(그램)에서 kg(킬로그램)으로 변환하는 함수의 타입을 쓸 때는 다음과 같이 표현해요.
let convertg2kg (x : float<g>) = x / 1000.0<g/kg>
측정 단위는 컴파일 타임 단위 검사에 사용되지만 런타임 환경에는 저장되지 않아요. 그래서 성능에는 영향을 주지 않아요.
측정 단위는 부동소수점 타입뿐 아니라 어떤 타입에도 적용할 수 있어요. 다만 차원이 붙은 양을 지원하는 것은 부동소수점 타입, 부호 있는 정수 타입, 그리고 소수(decimal) 타입뿐이에요. 따라서 측정 단위는 원시 타입과 그런 원시 타입을 담는 집합체(aggregate)에 쓰는 것이 합리적이에요.
다음 예시는 측정 단위의 사용을 보여줘요.
// Mass, grams.
[<Measure>] type g
// Mass, kilograms.
[<Measure>] type kg
// Weight, pounds.
[<Measure>] type lb
// Distance, meters.
[<Measure>] type m
// Distance, cm
[<Measure>] type cm
// Distance, inches.
[<Measure>] type inch
// Distance, feet
[<Measure>] type ft
// Time, seconds.
[<Measure>] type s
// Force, Newtons.
[<Measure>] type N = kg m / s^2
// Pressure, bar.
[<Measure>] type bar
// Pressure, Pascals
[<Measure>] type Pa = N / m^2
// Volume, milliliters.
[<Measure>] type ml
// Volume, liters.
[<Measure>] type L
// Define conversion constants.
let gramsPerKilogram : float<g kg^-1> = 1000.0<g/kg>
let cmPerMeter : float<cm/m> = 100.0<cm/m>
let cmPerInch : float<cm/inch> = 2.54<cm/inch>
let mlPerCubicCentimeter : float<ml/cm^3> = 1.0<ml/cm^3>
let mlPerLiter : float<ml/L> = 1000.0<ml/L>
// Define conversion functions.
let convertGramsToKilograms (x : float<g>) = x / gramsPerKilogram
let convertCentimetersToInches (x : float<cm>) = x / cmPerInch
다음 코드 예시는 무차원 부동소수점 수를 차원이 붙은 부동소수점 값으로 바꾸는 방법을 보여줘요. 1.0에 차원을 적용해 1.0을 곱하기만 하면 돼요. 이걸 degreesFahrenheit 같은 함수로 추상화할 수도 있어요.
또한 차원이 붙은 값을 무차원 부동소수점 수를 기대하는 함수에 넘길 때는 단위를 상쇄(cancel) 해야 하거나, float 연산자를 써서 float로 캐스팅해야 해요. 아래 예시에서는 printf가 무차원 양을 기대하기 때문에, 인자들을 1.0<degC>로 나눠주고 있어요.
[<Measure>] type degC // temperature, Celsius/Centigrade
[<Measure>] type degF // temperature, Fahrenheit
let convertCtoF ( temp : float<degC> ) = 9.0<degF> / 5.0<degC> * temp + 32.0<degF>
let convertFtoC ( temp: float<degF> ) = 5.0<degC> / 9.0<degF> * ( temp - 32.0<degF>)
// Define conversion functions from dimensionless floating point values.
let degreesFahrenheit temp = temp * 1.0<degF>
let degreesCelsius temp = temp * 1.0<degC>
printfn "Enter a temperature in degrees Fahrenheit."
let input = System.Console.ReadLine()
let parsedOk, floatValue = System.Double.TryParse(input)
if parsedOk
then
printfn "That temperature in Celsius is %8.2f degrees C." ((convertFtoC (degreesFahrenheit floatValue))/(1.0<degC>))
else
printfn "Error parsing input."
이 코드의 입력과 출력을 보여주는 실행 세션 예시는 다음과 같아요.
Enter a temperature in degrees Fahrenheit.
90
That temperature in degrees Celsius is 32.22.
측정 단위를 지원하는 원시 타입 (Primitive Types supporting Units of Measure)
다음 타입 또는 타입 약어 별칭들은 단위 주석을 지원해요.
| F# alias | CLR Type |
|---|---|
| float32/single | System.Single |
| float/double | System.Double |
| decimal | System.Decimal |
| sbyte/int8 | System.SByte |
| int16 | System.Int16 |
| int/int32 | System.Int32 |
| int64 | System.Int64 |
| byte/uint8 | System.Byte |
| uint16 | System.UInt16 |
| uint/uint32 | System.UInt32 |
| uint64 | System.UIn64 |
| nativeint | System.IntPtr |
| unativeint | System.UIntPtr |
예를 들어, 부호 없는 정수에 다음과 같이 주석을 달 수 있어요.
[<Measure>]
type days
let better_age = 3u<days>
이 기능에 부호 없는 정수 타입을 추가한 내용은 F# RFC FS-1091에 문서화되어 있어요.
미리 정의된 측정 단위 (Pre-defined Units of Measure)
FSharp.Data.UnitSystems.SI 네임스페이스에서 단위 라이브러리를 사용할 수 있어요. 여기에는 SI 단위가 두 가지 형태로 들어 있어요. UnitSymbols 하위 네임스페이스에는 기호 형태(길이를 뜻하는 m처럼)로, UnitNames 하위 네임스페이스에는 전체 이름(길이를 뜻하는 meter처럼)으로 제공돼요.
제네릭 단위 사용하기 (Using Generic Units)
연관된 측정 단위가 있는 데이터를 다루는 제네릭 함수를 작성할 수 있어요. 타입과 함께 제네릭 단위를 타입 매개변수로 지정하면 되는데, 다음 코드 예시처럼 사용해요.
// Distance, meters.
[<Measure>] type m
// Time, seconds.
[<Measure>] type s
let genericSumUnits ( x : float<'u>) (y: float<'u>) = x + y
let v1 = 3.1<m/s>
let v2 = 2.7<m/s>
let x1 = 1.2<m>
let t1 = 1.0<s>
// OK: a function that has unit consistency checking.
let result1 = genericSumUnits v1 v2
// Error reported: mismatched units.
// Uncomment to see error.
// let result2 = genericSumUnits v1 x1
제네릭 단위를 가진 컬렉션 타입 만들기 (Creating Collection Types with Generic Units)
다음 코드는 단위가 제네릭인 개별 부동소수점 값들로 구성된 집합체 타입을 만드는 방법을 보여줘요. 이렇게 하면 다양한 단위와 함께 동작하는 단일 타입을 만들 수 있어요. 또한 제네릭 단위는, 한 단위 집합을 가진 제네릭 타입이 다른 단위 집합을 가진 동일한 제네릭 타입과 서로 다른 타입이 되도록 보장해서 타입 안전성을 유지해 줘요. 이 기법의 핵심은 Measure 특성을 타입 매개변수에 적용할 수 있다는 점이에요.
// Distance, meters.
[<Measure>] type m
// Time, seconds.
[<Measure>] type s
// Define a vector together with a measure type parameter.
// Note the attribute applied to the type parameter.
type vector3D<[<Measure>] 'u> = { x : float<'u>; y : float<'u>; z : float<'u>}
// Create instances that have two different measures.
// Create a position vector.
let xvec : vector3D<m> = { x = 0.0<m>; y = 0.0<m>; z = 0.0<m> }
// Create a velocity vector.
let v1vec : vector3D<m/s> = { x = 1.0<m/s>; y = -1.0<m/s>; z = 0.0<m/s> }
런타임의 단위 (Units at Runtime)
측정 단위는 정적 타입 검사에 사용돼요. 부동소수점 값이 컴파일되면 측정 단위는 제거돼서, 런타임에는 단위가 사라져요. 따라서 런타임에 단위를 검사하는 기능을 구현하려는 시도는 불가능해요. 예를 들어 단위를 출력해 주는 ToString 함수를 구현하는 것은 불가능해요.
변환 (Conversions)
단위가 있는 타입(예: float<'u>)을 단위가 없는 타입으로 바꾸려면 표준 변환 함수를 사용하면 돼요. 예를 들어 float를 사용해 단위가 없는 float 값으로 변환할 수 있어요. 다음 코드를 확인해 볼게요.
[<Measure>]
type cm
let length = 12.0<cm>
let x = float length
단위가 없는 값을 단위가 있는 값으로 바꾸려면, 적절한 단위로 주석이 달린 1 또는 1.0 값을 곱하면 돼요. 다만 상호운용 계층을 작성할 때는 단위 없는 값을 단위 있는 값으로 변환할 수 있는 명시적 함수들도 있어요. 이런 함수는 FSharp.Core.LanguagePrimitives 모듈에 있어요. 예를 들어 단위 없는 float를 float<cm>으로 변환하려면 FloatWithMeasure를 사용하면 돼요.
open Microsoft.FSharp.Core
let height:float<cm> = LanguagePrimitives.FloatWithMeasure x