Haskell Servant 입문 (Strongly Typing)

Haskell Servant 입문 (Strongly Typing)

2022-10-04 last update

14 minutes reading 하스켈

Strongly typing in Haskell with Servant



강한 타입 지정을 사용해 servant server 의 어플리케이션을 구현하는 방법을 소개합니다.
이 기사의 소스 코드는 다음 리포지토리에서 찾을 수 있습니다.
htp : // 기주 b. 코 m / 아 l가 s / 하 s l - r ゔ

강한 타이핑에 의한 효과



Servant는 클라이언트와 서버에서 처리하는 데이터에 강한 유형 지정을 적용 할 수 있습니다.
즉, 취급하는 데이터에 타입을 적용하는 것만으로 제한을 걸 수 있습니다.
덧붙여서 같은 Haskell의 웹 프레임워크에서도 Scotty 등에서는 약한 형식화(형 정보의 누락?)밖에 행해지지 않습니다(Scotty에서는 Text형으로서 취급된다).

강한 타입 지정을 도입했을 경우의 장점과 단점을 나란히 해 봅니다.

장점


  • 형만으로 값의 validation 을 할 수 있습니다
    값이 입력되는 곳마다 validation 가 되어 있는지 어떤지를 체크할 필요는 없어집니다.

  • 단점


  • 형식 변환을 구현하는 데 시간이 걸립니다.
    그러나 테스트까지 포함하면 많은 경우에 시간을 단축할 수 있어야 합니다.

  • 위를 읽은 것만으로는 그 효과를 이해하는 것이 쉽지 않다고 생각하기 때문에 구현 예를 살펴 보겠습니다.

    API



    사용자 데이터를 다루는 API를 생각해보십시오.
    사용자 목록을 보는 기능과 신규 사용자를 만드는 기능을 실현합니다.

    Main.hs
    type StrongAPI = "users" :> QueryParam "age" Teenage :> Get '[JSON] [User]
                :<|> "users" :> ReqBody '[JSON] User :> Post '[JSON] User
    
    data User = User
        { name  :: Text
        , age   :: Teenage
        , email :: EmailAddress
        } deriving (Show, Eq, Generic, FromJSON, ToJSON)
    

    상기의 API 로 등장하는 User 형은 name, age, email 의 값을 가집니다. 각각 Text, Teenage, EmailAddress 의 형태를 가집니다.
    그 중 Teenage 와 EmailAddress 가 범용이 아닌 형태입니다.
    EmailAddress는 hackage에 게시된 email-validate 라이브러리에 정의되어 있습니다.
    Teenage 는 아래와 같이 독자적으로 정의한 형태입니다.

    Teenage.hs
    module Teenage
        ( Teenage
        , generateTeenage
        , teenage
        )where
    
    newtype Teenage = Teenage { teenage :: Integer }
        deriving (Read, Show, Eq, Ord)
    
    generateTeenage :: Integer -> Maybe Teenage
    generateTeenage x
        | 12 < x && x < 20 = Just $ Teenage x
        | otherwise = Nothing
    

    Teanage는 이름에서 알 수 있듯이 13에서 19까지의 정수 값을 가진 유형입니다. 예상치 못한 값을 할당하지 않기 위해 생성자는 숨겨져 있습니다.

    유형 변환



    API에서 정의한 것처럼 QueryParam과 ReqBody, Response (JSON)에서 Teenage, User (EmailAddress)를 사용하고 있습니다.
    형식 변환을 위해서는 다음 인스턴스 정의가 각각 필요합니다.
  • QueryParam -> FromHttpApiData
  • ReqBody -> FromJSON
  • JSON Response -> ToJSON

  • Main.hs
    instance FromHttpApiData Teenage where
        parseQueryParam x =
            let
                y = parseQueryParam x :: Either Text Integer
                in case y of
                    Right r -> maybeToEither "Teenage" (generateTeenage r)
                    Left l  -> Left "Teenage"
    
    instance FromJSON Teenage where
        parseJSON (Number s) =
            case generateTeenage (coefficient s) of
                Just n -> return n
                _      -> typeMismatch "Teenage" (Number s)
        parseJSON m = typeMismatch "Teenage" m
    
    instance ToJSON Teenage where
        toJSON = Number . flip scientific 0 . teenage
    
    instance FromJSON EmailAddress where
        parseJSON (String s) =
            case emailAddress (encodeUtf8 s) of
                Just e -> return e
                _      -> typeMismatch "EmailAddress" (String s)
        parseJSON m = typeMismatch "EmailAddress" m
    
    instance ToJSON EmailAddress where
        toJSON = String . decodeUtf8 . toByteString
    

    User 자체의 FromJSON, ToJSON instance 는 상기의 data deriving 로 정의하고 있으므로, 명시적으로 구현할 필요는 없습니다.

    서버 구현



    다음은 서버

    Main.hs
    userList :: [User]
    userList =  [ User "John Smith" (fromJust (generateTeenage 18)) (fromJust (emailAddress "[email protected]"))
                , User "Alice Jones" (fromJust (generateTeenage 14)) (fromJust (emailAddress "[email protected]"))
    
    strongApi :: Proxy StrongAPI
    strongApi = Proxy
    
    server :: Server StrongAPI
    server = users :<|> newUser
        where
            users Nothing  = return userList
            users (Just a) = return [ u | u <- userList, age u == a ]
            newUser u = return u
    
    app :: Application
    app = serve strongApi server
    
    
    main :: IO ()
    main = do
        run 8080 app
    

    주목하고 싶은 점은, 형태 변환의 개소에서의 구현 이외에서는 validation 를 명시적으로 써 있지 않은 것입니다.
    즉, 의도하지 않은 validation 누출이 존재하지 않으므로 잠재적인 결함을 줄일 수 있습니다.
    또, 몇번이나 같은 변환이 코드내에서 등장하는 경우에는 구현하는 코드량의 삭감에도 연결됩니다.
    이 기사의 예에서는 API의 엔드 포인트의 수나 구현량이 적기 때문에, 보통으로 validation 를 구현했을 경우에 비해 구현 효율의 향상을 거의 실감할 수 없을지도 모릅니다.

    동작 확인


  • buildstack build
  • servestack exec strongly-typing
  • test
  • $ curl "http://localhost:8080/users"
    [{"email":"[email protected]","age":18,"name":"John Smith"},{"email":"[email protected]","age":14,"name":"Alice Jones"}]
    
    $ curl "http://localhost:8080/users?age=18"
    [{"email":"[email protected]","age":18,"name":"John Smith"}]
    
    $ curl -H 'Accept: application/json' -H 'Content-type: application/json' -X POST -d '{"name":"hoge","age":19,"email":"[email protected]"}' "http://localhost:8080/users"
    {"email":"[email protected]","age":19,"name":"hoge"}
    
    $ curl -H 'Accept: application/json' -H 'Content-type: application/json' -X POST -d '{"name":"hoge","age":21,"email":"[email protected]"}' "http://localhost:8080/users"
    Error in $: expected Teenage, encountered Number
    
    $ curl -H 'Accept: application/json' -H 'Content-type: application/json' -X POST -d '{"name":"hoge","age":13,"email":"hoge"}' "http://localhost:8080/users"
    Error in $: expected EmailAddress, encountered String
    

    앞의 3개의 요청이 성공했으며, 2개의 요청이 의도한 대로 실패했습니다.
    사양대로 요청을 보내면 올바르게 응답이 반환됩니다.
    형식에 의한 validation 가 정상적으로 기능하고 있어, 범위외의 age 나 well-formed 가 아닌 email 을 주면(자) 에러 (400 Bad Request)가 돌아옵니다.

    참고문헌


  • 함수 프로그래밍 실천 입문 (오카와 토쿠유키) 제6장