Understanding the difference between Lens and Optics ?~ operator

I’m encountering a difference in behavior in the ?~ operator between these two libraries that I’m not able understand. The mkClaims function works when using the lens library but fails with optics, and it produces an error. Is there a different operator I should be using if I already have optics elsewhere in my project?

{-# LANGUAGE ImportQualifiedPost #-}
{-# LANGUAGE OverloadedStrings #-}

import Control.Lens.Operators ((?~)) -- This succeeds
-- import Optics.Operators ((?~))    -- This fails
import Crypto.JOSE.JWA.JWK
  ( KeyMaterialGenParam (OKPGenParam)
  , OKPCrv (Ed448)
  )
import Crypto.JOSE.JWK (JWK, bestJWSAlg, genJWK)
import Crypto.JWT
  ( Audience (Audience)
  , ClaimsSet
  , HasClaimsSet (..)
  , JWTError
  , NumericDate (..)
  , SignedJWT
  , StringOrURI
  , defaultJWTValidationSettings
  , emptyClaimsSet
  , encodeCompact
  , newJWSHeaderProtected
  , runJOSE
  , signClaims
  , verifyClaims
  )
import Data.Aeson.Encode.Pretty (encodePretty)
import Data.ByteString.Lazy.UTF8 (toString)
import Data.Function ((&))
import Data.Text.Lazy qualified as T (unpack)
import Data.Text.Lazy.Encoding (decodeUtf8)
import Data.Time.Clock (addUTCTime, getCurrentTime)

jw :: IO JWK
jw = genJWK $ OKPGenParam Ed448

app :: StringOrURI
app = "qa"

tokenUri :: StringOrURI
tokenUri = "http://localhost:8080/realms/qa/protocol/openid-connect/token"

mkClaims :: IO ClaimsSet
mkClaims = do
  t <- getCurrentTime
  pure $
    emptyClaimsSet
      & claimIss ?~ app
      & claimSub ?~ app
      & claimAud ?~ Audience [tokenUri]
      & claimIat ?~ NumericDate t
      & claimExp ?~ NumericDate (addUTCTime 3600 t) -- expires in 1 hour

doJwtSign :: JWK -> ClaimsSet -> IO (Either JWTError SignedJWT)
doJwtSign jwk_ claims = runJOSE $ do
  alg_ <- bestJWSAlg jwk_
  signClaims jwk_ (newJWSHeaderProtected alg_) claims

doJwtVerify :: JWK -> SignedJWT -> IO (Either JWTError ClaimsSet)
doJwtVerify jwk_ jwt_ = runJOSE $ do
  let config = defaultJWTValidationSettings (== tokenUri)
  verifyClaims config jwk_ jwt_

main :: IO ()
main = do
  j <- jw

  putStrLn $ toString $ encodePretty j
  c <- mkClaims
  r <- doJwtSign j c
  case r of
    Left err -> print $ "Couldn't sign" ++ show err
    Right s -> do
      putStrLn $ T.unpack $ decodeUtf8 $ encodeCompact s
      v <- doJwtVerify j s
      case v of
        Left _ -> putStrLn "Verification error"
        Right cs -> putStrLn $ toString $ encodePretty cs
    • Couldn't match expected type: optics-core-0.4.1.1:Optics.Internal.Optic.Optic
                                      k0 is0 ClaimsSet a1 a0 (Maybe StringOrURI)
                  with actual type: (Maybe StringOrURI -> f0 (Maybe StringOrURI))
                                    -> a9 -> f0 a9
    • Probable cause: ‘claimIss’ is applied to too few arguments
      In the first argument of ‘(?~)’, namely ‘claimIss’
      In the second argument of ‘(&)’, namely ‘claimIss ?~ app’
      In the first argument of ‘(&)’, namely
        ‘emptyClaimsSet & claimIss ?~ app’
   |
48 |       & claimIss ?~ app
   |         ^^^^^^^^
...

The problem is that the optic representations used by the lens and optics packages are different. lens uses the so-called van Laarhoven (VL) representation where each kind of optic is represented as a transparent type synonym, whereas in optics they are represented as different opaque types. The claimIss lens (provided by the jose library I presume) adopts the VL representation, hence doesn’t work with optics as is. However, optics provides conversion functions for these kinds of optics. In this case specifically, you can use lensVL claimIss instead of claimIss, and it should just work.

4 Likes