I’m trying to make a function to check HTTP status codes, generate errors for unhandled ones, and pass the handled ones through to another function, like (simplified):
type unexpected_status =
{ path : string
; expected : Cohttp.Code.status_code list
; got : Cohttp.Code.status_code }
let get ?(ok_statuses=[ `OK ]) path =
Cohttp_lwt_unix.get path
>>= fun (res, body) ->
Cohttp_lwt.Body.to_string body
>|= fun body ->
match Cohttp_lwt.Response.status res with
| status when List.exists ok_statuses ~f:((=) status) ->
Ok (status, body)
| status ->
Lwt.return_error
(`Unexpected_status { path
; expected = ok_statuses
; got = status })
What’s annoying is that this function returns (Cohttp.Code.status_code, string) Lwt_result.t
, but I’d really like to make it return only the statuses in ~ok_statuses
(so I don’t have to have | _ -> assert false
in every variant using this).
I think I’m trying to give it this type (although, honestly the <
, >
stuff still confuses me for some reason):
val get
: ?ok_statuses:([< Cohttp.Code.status_code ] as 'status) list
-> string
-> ('status * string) Lwt_result.t
Is it possible to write this function in such a way that it does that?
I realized one piece of this is probably what status when List.exists ok_statuses ~f:((=) status)
doesn’t give status
the type I want, so I’d probably want to use the object from the list instead (List.find ok_statuses ~f:((=) status)
, but I’m still not sure how to make that have a less general type than Cohttp.Code.status_code
(or if it’s possible at all).
This sounds doable with the right coercion. The right idea is probably to try to coerce your status to the smaller type first:
type core = [`A|`B]
type all = [ core | `C ]
let to_core : all -> (core,all) result = function
| #core as x -> Ok x
| x -> Error x
then check if it belongs to the list
1 Like
It seems to be doable following the idea of returning the member of the list. The trick is to make an equality predicate which don’t assume same-type arguments, and one solution is to serialize the values, e.g. using Marshall
:
let poly_equal x y = Marshal.to_string x = Marshal.to_string y
let rec pick : 'a -> 'b list -> 'b option = fun x -> function
| [] -> None
| y :: ys -> if poly_equal x y then Some y else pick x ys
;;
val poly_equal : 'a -> 'b -> bool = <fun>
val pick : 'a -> 'b list -> 'b option = <fun>
This is not ideal. I think solutions of this kind will necessarily depend on “extra”-polymorphic functions provided by the OCaml core library. [see next post]
If ok_statuses
has a default value, it’s type will be a super-type of that value’s type, i.e. [> `OK]
in the given case, so you may want isolate the default case into a separate function.
Using Marshal.to_string x []
to do any comparison sounds both dangerous:
Marshal.to_string [] [] = Marshal.to_string 0 []
and incredibly brittle: the marshaled string will depend on the amount of sharing in the both structure. For instance, in the toplevel, this equality is false
Marshal.to_string (let f = 0. in [f;f]) [] <> Marshal.to_string [0.;0.] []
but Flambda will share the constant 0. which makes the equality true
Marshal.to_string (let f = 0. in [f;f]) [] = Marshal.to_string [0.;0.] []
in this case.
The right way to do this is to coerce to the common subtype
let subtype_equal (* : al l -> [<all] -> bool *) = fun x y -> x = (y:>all)
let pick x = List.find_opt (subtype_equal x) l
But if the picked value is equal to x
, it is simpler to just do the conversion directly
let conv = function
| #core as x -> x
| _ -> raise Not_found
Nice, I didn’t think of subtype_equal
. That should make an acceptable option. I agree on the issues using Marshall
.
Adapting your conv
and to_core
to the original question, the #core
is only known as a type parameter, so that may not work.
Thanks both of you! This version worked for me:
let subtype_equal = fun x y -> x = (y:>all)
let pick x = List.find_opt (subtype_equal x) l
This is what I ended up with: https://github.com/brendanlong/feedbin-ocaml/commit/4b2854433e18f8af7d97855390e7ea1ee2167e11
I’m still not sure if I’m missing something with the | #variant_type ->
version though. I think for what I want to do, I would need to refer to a type parameter, but something like | #'status
is a syntax error, and if I start my function declaration with (type status)
, Merlin tells me that status
isn’t a polymorphic variant type for some reason.