Unable to use s-expressions - ocaml

I'm following Real World OCaml to get started with the language, and, at one point, I am to make use of s-expressions in a module signature. Here's my mli file:
open Core.Std
(** Configuration type for query handlers *)
type config with sexp
(** Name of the query handler *)
val name : string
(** Query handler abstract type *)
type t
(** Create a query handler from an existing [config] *)
val create : config -> t
(** Evaluate a query, where both input and output an s-expressions. *)
val eval : t -> Sexp.t -> Sexp.t Or_error.t
But, when compiling an implementation of that interface, I get the following error:
File "Query_Handler.mli", line 4, characters 12-16:
Error: Syntax error
Command exited with code 2.
So I opened utop to try with sexp on a simpler example:
module type Test = sig
type t with sexp
end;;
But I get the following error:
Error: Parse Error: "end" expected after [sig_items] (in [module type])
However, sexplib is installed and neither the book nor my searches on the Internet mention any "prerequisites" for using this syntax.
I feel like I'm missing something. Any idea? :(

This is because the sexp library had been rewritten to use Extension Point, instead of Camlp4.
open Core.Std
module type Query_handler = sig
(** Configuration for a query handler. Note that this can be
Converted to and from an s-expression *)
type config [##deriving sexp]
(** The name of the query-handling service *)
val name : string
(** The state of the query handler *)
type t
(** Create a new query handler from a config *)
val create : config -> t
(** Evaluate a given query, where both input and output are
s-expressions *)
val eval : t -> Sexp.t -> Sexp.t Or_error.t
end
module Unique = struct
type config = int [##deriving sexp]
type t = { mutable next_id: int }
let name = "unique"
let create start_at = { next_id = start_at }
let eval t sexp =
match Or_error.try_with (fun () -> unit_of_sexp sexp) with
| Error _ as err -> err
| Ok () ->
let response = Ok (Int.sexp_of_t t.next_id) in
t.next_id <- t.next_id + 1;
response
end
module List_dir = struct
type config = string [##deriving sexp]
type t = { cwd: string }
(** [is_abs p] Returns true if [p] is an absolute path *)
let is_abs p =
String.length p > 0 && p.[0] = '/'
let name = "ls"
let create cwd = { cwd }
let eval t sexp =
match Or_error.try_with (fun () -> string_of_sexp sexp) with
| Error _ as err -> err
| Ok dir ->
let dir =
if is_abs dir then dir
else Filename.concat t.cwd dir
in
Ok (Array.sexp_of_t String.sexp_of_t (Sys.readdir dir))
end
module type Query_handler_instance = sig
module Query_handler : Query_handler
val this : Query_handler.t
end
let build_instance (type a)
(module Q : Query_handler with type config = a)
config =
(module struct
module Query_handler = Q
let this = Q.create config
end : Query_handler_instance)
let build_dispatch_table handlers =
let table = String.Table.create () in
List.iter handlers
~f:(fun ((module I : Query_handler_instance) as instance) ->
Hashtbl.replace table ~key:I.Query_handler.name ~data:instance);
table
let dispatch dispatch_table name_and_query =
match name_and_query with
| Sexp.List [Sexp.Atom name; query] ->
begin match Hashtbl.find dispatch_table name with
| None ->
Or_error.error "Could not find matching handler"
name String.sexp_of_t
| Some (module I : Query_handler_instance) ->
I.Query_handler.eval I.this query
end
| _ ->
Or_error.error_string "malformed query"
let rec cli dispatch_table =
printf ">>> %!";
let result =
match In_channel.input_line stdin with
| None -> `Stop
| Some line ->
match Or_error.try_with (fun () -> Sexp.of_string line) with
| Error e -> `Continue (Error.to_string_hum e)
| Ok query ->
begin match dispatch dispatch_table query with
| Error e -> `Continue (Error.to_string_hum e)
| Ok s -> `Continue (Sexp.to_string_hum s)
end;
in
match result with
| `Stop -> ()
| `Continue msg ->
printf "%s\n%!" msg;
cli dispatch_table
let unique_instance = build_instance (module Unique) 0
let list_dir_instance = build_instance (module List_dir) "/var"
module Loader = struct
type config = (module Query_handler) list sexp_opaque [##deriving sexp]
type t = { known : (module Query_handler) String.Table.t
; active : (module Query_handler_instance) String.Table.t
}
let name ="loader"
let create known_list =
let active = String.Table.create () in
let known = String.Table.create () in
List.iter known_list
~f:(fun ((module Q : Query_handler) as q) ->
Hashtbl.replace known ~key:Q.name ~data:q);
{ known; active }
let load t handler_name config =
if Hashtbl.mem t.active handler_name then
Or_error.error "Can't re-register an active handler"
handler_name String.sexp_of_t
else
match Hashtbl.find t.known handler_name with
| None ->
Or_error.error "Unknown handler" handler_name String.sexp_of_t
| Some (module Q : Query_handler) ->
let instance =
(module struct
module Query_handler = Q
let this = Q.create (Q.config_of_sexp config)
end : Query_handler_instance)
in
Hashtbl.replace t.active ~key:handler_name ~data:instance;
Ok Sexp.unit
let unload t handler_name =
if not (Hashtbl.mem t.active handler_name) then
Or_error.error "Handler not active" handler_name String.sexp_of_t
else if handler_name = name then
Or_error.error_string "It's unwise to unload yourself"
else (
Hashtbl.remove t.active handler_name;
Ok Sexp.unit
)
type request =
| Load of string * Sexp.t
| Unload of string
| Known_services
| Active_services [##deriving sexp]
let eval t sexp =
match Or_error.try_with (fun () -> request_of_sexp sexp) with
| Error _ as err -> err
| Ok resp ->
match resp with
| Load (name,config) -> load t name config
| Unload name -> unload t name
| Known_services ->
Ok [%sexp ((Hashtbl.keys t.known ) : string list)]
| Active_services ->
Ok [%sexp ((Hashtbl.keys t.active) : string list)]
end

This is my ~/.ocamlinit; just comment out the camlp4. utop should work happy.
#use "topfind";;
#warnings "+9"
#thread;;
(*camlp4;;*)
#require "core.top";;
#require "core_extended";;
#require "core_bench";;
#require "ppx_jane";;
#require "ctypes";;
#require "ctypes.foreign";;

Related

This expression has type H2_lwt_unix.Client.t but an expression was expected of type 'weak702 Lwt.t

How to deal with 'a Lwt objects in a function?
My code is
Array.map (fun conn -> let* resp = (call_server conn
(RequestVoteArg({
candidateNumber = myState.myPersistentState.id;
term = myState.myPersistentState.currentTerm;
lastlogIndex = (Array.get myState.myPersistentState.logs ((Array.length myState.myPersistentState.logs) - 1)).index;
lastlogTerm = (Array.get myState.myPersistentState.logs ((Array.length myState.myPersistentState.logs) - 1)).term
}))) in (match resp with
| Error(s) -> Printf.printf "requestVote: connection failed: %s" s
| Ok(repl, s) ->
(match repl with
| RequestVoteRet(repl) ->
if repl.voteGranted then current_vote := !current_vote + 1;
if not (repl.term = (-1l)) then myState.myPersistentState.currentTerm <- repl.term;
Printf.printf "requestVote: status: %s" s
| _ -> failwith "Should not reach here")); conn) peers
But there is an error: This expression (conn in the end) has type H2_lwt_unix.Client.t but an expression was expected of type 'weak702 Lwt.t.
peers is an array of connections (type: H2_lwt_unix.Client.t).
The definition of call_server is:
val call_server: H2_lwt_unix.Client.t -> protobufArg -> (Types.protobufRet * Grpc.Status.t, Grpc.Status.t) result Lwt.t
and
let build_connection addr port =
let* addrs =
Lwt_unix.getaddrinfo addr (string_of_int port)
[ Unix.(AI_FAMILY PF_INET) ]
in
let socket = Lwt_unix.socket Unix.PF_INET Unix.SOCK_STREAM 0 in
let* () = Lwt_unix.connect socket (List.hd addrs).Unix.ai_addr in
let error_handler _ = print_endline "error" in
let connection =
H2_lwt_unix.Client.create_connection ~error_handler socket
in connection
let call_server connection req =
let enc = Pbrt.Encoder.create() in
match req with
| RequestVoteArg(s) ->
let proto_s =
Proto.Proto_types.default_request_vote_arg ~candidate_number:s.candidateNumber ~term:s.term ~lastlog_term:s.lastlogTerm ~lastlog_index:s.lastlogIndex ()
in
Proto.Proto_pb.encode_request_vote_arg proto_s enc;
Client.call ~service:"raft.Proto" ~rpc:"RequestVote"
~do_request:(H2_lwt_unix.Client.request connection ~error_handler:ignore)
~handler:
(Client.Rpc.unary (Pbrt.Encoder.to_string enc) ~f:(fun decoder ->
(let+ decoder = decoder in
(match decoder with
| Some (decoder) ->
let decoder = Pbrt.Decoder.of_string decoder in
let reply = Proto.Proto_pb.decode_request_vote_reply decoder in
RequestVoteRet( { term=reply.term; voteGranted=reply.vote_granted } )
| None -> RequestVoteRet( { term=(-1l); voteGranted=false } ))))) ()
| AppendEntriesArg(s) ->
let proto_s =
Proto.Proto_types.default_append_entries_arg ~term: s.term ~leader_id:s.leaderID ~next_log_index:s.nextLogIndex ~next_log_term:s.nextLogTerm ~entries:(List.map (fun lg -> Proto.Proto_types.default_log ~command: lg.command ~term:lg.term ~index: lg.index ()) (Array.to_list (s.entries)) ) ()
in
Proto.Proto_pb.encode_append_entries_arg proto_s enc;
Client.call ~service:"raft.Proto" ~rpc:"AppendEntries"
~do_request:(H2_lwt_unix.Client.request connection ~error_handler:ignore)
~handler:
(Client.Rpc.unary (Pbrt.Encoder.to_string enc) ~f:(fun decoder ->
let+ decoder = decoder in
match decoder with
| Some(decoder) ->
let decoder = Pbrt.Decoder.of_string decoder in
let reply = Proto.Proto_pb.decode_append_entries_reply decoder in
AppendEntriesRet( { term=reply.term; success=reply.success } )
| None -> AppendEntriesRet( { term=(-1l); success=false } ))) ()
Your code can be simplified to:
let f (conn:H2_lwt_unix.Client.t) =
let* resp = ... in
...; conn
which fails because conn is a connection and not a promise of returning a connection.
The easiest fix is to wrap this value inside a Lwt promise with either
let f (conn:H2_lwt_unix.Client.t) =
let* resp = ... in
...; Lwt.return conn
or
let f (conn:H2_lwt_unix.Client.t) =
let+ resp = ... in
...; conn

OCaml serializing a (no args) variant as a "string enum" (via Yojson)

Say I am building a record type:
type thing {
fruit: string;
}
But I want the possible values of fruit to be constrained to a fixed set of strings.
It seems natural to model this in OCaml as a variant, e.g.:
type fruit = APPLE | BANANA | CHERRY
type thing {
fruit: fruit;
}
Okay so far.
But if I use [##deriving yojson] on these types then the serialized output will be like:
{ "fruit": ["APPLE"] }
By default Yojson wants to serialize a variant as a tuple of [<name>, <args>...] which... I can see the logic of it, but it is not helpful here.
I want it to serialize as:
{ "fruit": "APPLE" }
Making use of a couple of ppx deriving plugins I managed to build this module to de/serialize as I want:
module Fruit = struct
type t = APPLE | BANANA | CHERRY [##deriving enum, variants]
let names =
let pairs i (name, _) = (name, (Option.get (of_enum i))) in
let valist = List.mapi pairs Variants.descriptions in
List.to_seq valist |> Hashtbl.of_seq
let to_yojson v = `String (Variants.to_name v)
let of_yojson = function
| `String s -> Hashtbl.find_opt names s
|> Option.to_result ~none:(Printf.sprintf "Invalid value: %s" s)
| yj -> Error (Printf.sprintf "Invalid value: %s" (Yojson.Safe.to_string yj))
end
Which works fine... but I have some other "string enum" variants I want to treat the same way. I don't want to copy and paste this code every time.
I got as far as this:
module StrEnum (
V : sig
type t
val of_enum : int -> t option
module Variants : sig
val descriptions : (string * int) list
val to_name : t -> string
end
end
) = struct
type t = V.t
let names =
let pairs i (name, _) = (name, (Option.get (V.of_enum i))) in
let valist = List.mapi pairs V.Variants.descriptions in
List.to_seq valist |> Hashtbl.of_seq
let to_yojson v = `String (V.Variants.to_name v)
let of_yojson = function
| `String s -> Hashtbl.find_opt names s
|> Option.to_result ~none:(Printf.sprintf "Invalid StrEnum value: %s" s)
| yj -> Error (Printf.sprintf "Invalid StrEnum value: %s" (Yojson.Safe.to_string yj))
end
module Fruit = struct
type t = APPLE | BANANA | CHERRY [##deriving enum, variants]
end
module FruitEnum = StrEnum (Fruit)
That much seems to type-check, and I can:
utop # Yojson.Safe.to_string (FruitEnum.to_yojson Fruit.APPLE);;
- : string = "\"APPLE\""
utop # FruitEnum.of_yojson (Yojson.Safe.from_string "\"BANANA\"");;
- : (FruitEnum.t, string) result = Ok Fruit.BANANA
...but when I try to:
type thing {
fruit: FruitEnum.t;
}
[##deriving yojson]
I get Error: Unbound value FruitEnum.t
It seems to be because I am re-exporting type t = V.t from the variant's module, I don't really understand though. (Or is it because the yojson ppx can't "see" the result of the functor properly?)
How can I fix this?
I would also like to be able to skip defining the variant module separately and just do:
module Fruit = StrEnum (struct
type t = APPLE | BANANA | CHERRY [##deriving enum, variants]
end)
...but this gives the error:
Error: This functor has type
functor
(V : sig
type t
val of_enum : int -> t option
module Variants :
sig
val descriptions : (string * int) list
val to_name : t -> string
end
end)
->
sig
type t = V.t
val names : (string, t) Hashtbl.t
val to_yojson : t -> [> `String of string ]
val of_yojson : Yojson.Safe.t -> (t, string) result
end
The parameter cannot be eliminated in the result type.
Please bind the argument to a module identifier.
and I don't understand what is wrong.
Regarding the last error, it's because OCaml requires a 'stable path' to types inside modules so it can refer to them. A stable path is a named path to a type, e.g. Fruit.t.
By contrast, StrEnum(struct type t = ... end).t is not a stable path because the type t is referencing a type t in the module literal which does not have a name.
Long story short, you basically can't skip defining the variant module separately. But it's simple to do it in two steps:
module Fruit = struct
type t = ...
end
module Fruit = StrEnum(Fruit)
The second definition refers to the first and shadows it. Shadowing is a well-known and often-used technique in OCaml.
Overall, I'm not sure all this PPX machinery is actually justified. You can pretty easily hand-write converter functions, e.g.
let to_yojson = function
| APPLE -> `String "APPLE"
| BANANA -> `String "BANANA"
| CHERRY -> `String "CHERRY"
Well, I was curious to have a go at writing a PPX deriver to perform this transformation.
Here's what I ended up with:
open Ppxlib
module List = ListLabels
let make_methods ~(loc : location) ~(is_poly : bool) (constructors : constructor_declaration list) =
let (module Ast) = Ast_builder.make loc in
let v_patt = match is_poly with
| true -> fun name -> Ast.ppat_variant name None
| false -> fun name -> Ast.ppat_construct { txt = (Lident name); loc } None
and v_expr = match is_poly with
| true -> fun name -> Ast.pexp_variant name None
| false -> fun name -> Ast.pexp_construct { txt = (Lident name); loc } None
in
let (to_cases, of_cases) =
List.map constructors ~f:(
fun cd ->
let name = cd.pcd_name.txt in
let to_case = {
pc_lhs = v_patt name;
pc_guard = None;
pc_rhs = [%expr `String [%e Ast.estring name] ];
} in
let of_case = {
pc_lhs = Ast.ppat_variant "String" (Some (Ast.pstring name));
pc_guard = None;
pc_rhs = [%expr Ok ([%e v_expr name]) ];
} in
(to_case, of_case)
)
|> List.split
in
let of_default_case = {
pc_lhs = [%pat? yj ];
pc_guard = None;
pc_rhs = [%expr Error (Printf.sprintf "Invalid value: %s" (Yojson.Safe.to_string yj)) ];
} in
let of_cases = of_cases # [of_default_case] in
let to_yojson = [%stri let to_yojson = [%e Ast.pexp_function to_cases]] in
let of_yojson = [%stri let of_yojson = [%e Ast.pexp_function of_cases] ] in
[to_yojson; of_yojson]
let type_impl ~(loc : location) (td : type_declaration) =
match td with
| {ptype_kind = (Ptype_abstract | Ptype_record _ | Ptype_open); _} ->
Location.raise_errorf ~loc "Cannot derive yojson_str_enum for non variant types"
| {ptype_kind = Ptype_variant constructors; _} -> begin
let invalid_constructors =
List.filter_map constructors ~f:(
fun cd -> match cd.pcd_args with
| (Pcstr_tuple [] | Pcstr_record []) -> None
| _ -> Some (cd)
)
in
if (List.length invalid_constructors) > 0 then
Location.raise_errorf ~loc "Cannot derive yojson_str_enum for variant types with constructor args";
match is_polymorphic_variant td ~sig_:false with
| `Definitely | `Maybe -> make_methods ~loc ~is_poly:true constructors
| `Surely_not -> make_methods ~loc ~is_poly:false constructors
end
let generate_impl ~ctxt (_rec_flag, type_declarations) =
(* [loc] is "location", not "lines of code" *)
let loc = Expansion_context.Deriver.derived_item_loc ctxt in
List.map type_declarations ~f:(type_impl ~loc)
|> List.concat
let yojson_str_enum =
Deriving.add
"yojson_str_enum"
~str_type_decl:(Deriving.Generator.V2.make_noarg generate_impl)
to make usable it needs a dune file something like:
(library
(kind ppx_rewriter)
(name <lib name>)
(preprocess (pps ppxlib.metaquot))
(libraries yojson ppxlib))
After adding <lib name> to the pps in your dune file, usage is like:
module Fruit = struct
type t = APPLE | BANANA | CHERRY [##deriving yojson_str_enum]
end
It seems to work fine for my use case. It might be extended per the comment by #Yawar to take args allowing to specify to/from string transform functions for the variant labels. But I was happy just with Fruit.APPLE -> "APPLE" for now. I should also implement the sig_type_decl version.
One part I am a bit uncertain about is this:
match is_polymorphic_variant td ~sig_:false with
| `Definitely | `Maybe -> make_methods ~loc ~is_poly:true constructors
| `Surely_not -> make_methods ~loc ~is_poly:false constructors
I am not very clear when the `Maybe case occurs or how it should most correctly be handled, or if there is a better way of detecting "backtick variants" than using the is_polymorphic_variant method from ppxlib.

Functor in Ocaml

I have a problem with functor in Ocaml. I have this situation:
module type EveryType =
sig
type t
val str : t -> string
end;;
module type StackInterface =
functor (El : EveryType) ->
sig
type el = El.t
type stack
exception EmptyStackException
val empty : stack
val pop : stack -> stack
val top : stack -> el
val push : stack -> el -> stack
val str : stack -> string
end;;
module StackImpl (El : EveryType) =
struct
type el = El.t
type stack = Empty | Node of el * stack
exception EmptyStackException
let empty = Empty
let pop s =
match s with
| Empty -> raise EmptyStackException
| Node(_, t) -> t
let top s =
match s with
| Empty -> raise EmptyStackException
| Node(h, _) -> h
let push s el = Node(el, s)
let str s =
let rec str s =
match s with
| Node(h, Empty) -> El.str h ^ ")"
| Node(h, t) -> El.str h ^ ", " ^ str t
| _ -> ""
in
if s == Empty then
"Stack()"
else
"Stack(" ^ str s
end;;
module Stack = (StackImpl : StackInterface);;
module TypeChar =
struct
type t = char
let str c = Printf.sprintf "%c" c
end;;
module StackChar = Stack(TypeChar);;
module CheckExp(St : module type of StackChar) =
struct
let checkExp str =
let rec checkExp str stk =
try
match str with
| [] -> true
| '(' :: t -> checkExp t (St.push stk '(')
| ')' :: t -> checkExp t (St.pop stk)
| _ :: t -> checkExp t stk
with St.EmptyStackException -> false
in checkExp (explode str) St.empty
end;;
I create a Stack with functor to have a stack of every type. Now I want to use this stack (with type char) in a function that check parantesis into an expression. But compiler gives me this error: Unbound module type StackChar refered to line module CheckExp(St : StackChar) =
What have I wrong???
StackChar is a module, but what you need for a functor is a module type. It wouldn't be much of a functor if you always pass it the same module. The simplest fix for this is to replace it with module type of StackChar:
module CheckExp(St : module type of StackChar) =
struct
...
end
But are you sure you actually need a functor here?

OCaml Hashtbl/0.t and Hashtbl/-1.t

I am quite new to OCaml, so I am not sure what the following error message means (specifically the /0 and the /-1):
Error: This expression has type (string, string) Hashtbl/0.t
but an expression was expected of type ('a, 'b) Hashtbl/-1.t
I am passing a Hashtbl.t into Hashtbl.find and this error shows up. I am unclear as to how the /0 and /-1 came in, and what they actually mean.
Here's a minimal working example to demonstrate my issue:
open Core_kernel.Std
let file_to_hashtbl filename =
let sexp_to_hashtbl_str = Sexplib.Conv.hashtbl_of_sexp
string_of_sexp string_of_sexp
in In_channel.with_file
filename ~f:(fun ch -> (Sexp.input_sexp ch |> sexp_to_hashtbl_str))
let ht = file_to_hashtbl "test"
let t1_val = match Hashtbl.find ht "t1" with
| Some v -> v
| None -> assert false
let () = print_endline t1_val
Let's show you an example :
If I write
type t = A;;
let x = A;;
type t = B;;
let y = B;;
x = y;;
Error: This expression has type t/1561 but an expression was expected of type
t/1558
This is because in the interpreter you can declare multiple types with the same name and associate values to these types. But here, as you can see, x and y are not of the same type but both the types are named t so the interpreter tries to tell you the types are both named t but are not the same.
[Compilation]
If I wanted to compile this, I would have to declare
typea.ml
type t = A
let x = A
typeb.ml
type t = B
let y = B
main.ml
open Typea
open Typeb
x = y
If I compile this I will have
Error: This expression has type Typeb.t
but an expression was expected of type Typea.t
What lesson should you learn from this ? Stop interpreting, compile !
Now that I managed to compile your file, I got an error too but much more explicit :
Error: This expression has type (string, string) Hashtbl.t
but an expression was expected of type
('a, 'b) Core_kernel.Std.Hashtbl.t =
('a, 'b) Core_kernel.Core_hashtbl.t
[Explanation and correction]
Since I'm too nice, here is your file corrected :
let file_to_hashtbl filename =
(* open the namespace only where needed *)
let open Core_kernel.Std in
let sexp_to_hashtbl_str = Sexplib.Conv.hashtbl_of_sexp
string_of_sexp string_of_sexp
in In_channel.with_file
filename ~f:(fun ch -> (Sexp.input_sexp ch |> sexp_to_hashtbl_str));;
let ht = file_to_hashtbl "test"
let t1_val =
try
Hashtbl.find ht "t1"
with Not_found -> assert false
let () = print_endline t1_val
Your error was that you opened Core_kernel.Std as a global namespace so when you wrote Hashtbl.find it looked first in Core_kernel.Std and not in the standard library.
What I did is open Core_kernel.Std in the function that needs it, not in the whole file (so it's a local namespace) (a good habit to take).
So, as you can see, the problem was that you had two definition of the type Hashtbl.t (one in Core_kernel.Std and one in the standard library) and OCaml ain't no fool, boy, he knows when you're wrong but he is hard to understand since he only speak for those who can hear. :-D
P.S. : You had an error in your Hashtbl.find because it doesn't return an option but the found value or raise a Not_found exception if no value was found. I corrected it too. ;-)
Apparently, it is just a matter of missing semi-columns, the foloowing code compiles :
open Core_kernel.Std;;
let file_to_hashtbl filename =
let sexp_to_hashtbl_str = Sexplib.Conv.hashtbl_of_sexp
string_of_sexp string_of_sexp
in In_channel.with_file
filename ~f:(fun ch -> (Sexp.input_sexp ch |> sexp_to_hashtbl_str));;
let ht = file_to_hashtbl "test"
let t1_val = match Hashtbl.find ht "t1" with
| Some v -> v
| None -> assert false
let () = print_endline t1_val
But, I do not know how to interpret the error message neither.

Eliom client to client messaging - Eref scope issue

I have been trying to gain a better understanding of the Eliom functionality for communication lately and to do so I tried to build a simple web page that allows users to send messages to each other.
The web page works fine if I log in as one user via Firefox and a second user via Chrome. But when I log in two different users in the same browser and try to send a message from one user to the other (i.e. from one tab to another tab), any message sent is displayed on all tabs, not the intended recipient's tab only.
I believe that I may have some issues with the chosen scope for erefs or where I am setting/getting the scope and erefs (toplevel vs. service definition).
I am trying to correct my mistake so that two users can be logged into two different tabs of the same browser and send messages back and forth to each other and the messages are only displayed on the correct users tabs.
Note: Some of this code is taken from the Eliom site tutorial at:
http://ocsigen.org/tuto/4.2/manual/how-to-implement-a-notification-system
My .eliom file:
(* Example website login: localhost:8080/?user_num=1 *)
{shared{
open Eliom_lib
open Eliom_content
open Html5
open Html5.F
open Eliom_registration
open Eliom_parameter
}}
module Channel_example_app =
Eliom_registration.App (
struct
let application_name = "channel_example"
end)
let main_service =
Eliom_service.App.service ~path:[] ~get_params:(string "user_num") ()
let new_message_action =
Eliom_service.Http.post_coservice'
~post_params:(string "from_user_id" ** string "to_user_id" ** string "msg") ()
(* Set the scope used by all erefs *)
let eref_scope = Eliom_common.default_process_scope
(* Create a channel eref *)
let channel_ref =
Eliom_reference.Volatile.eref_from_fun
~scope:eref_scope
(fun () ->
let (s, notify) = Lwt_stream.create () in
let c = Eliom_comet.Channel.create s in
(c, notify)
)
(* Reactive string eref *)
let react_string_ref =
Eliom_reference.Volatile.eref_from_fun
~scope:eref_scope
(fun () ->
let (client_string, send_client_string) :
(string React.E.t * (?step:React.step -> string -> unit) ) =
React.E.create ()
in
(client_string, send_client_string)
)
(* Reactive string to display the users session group *)
let react_session_group_ref =
Eliom_reference.Volatile.eref_from_fun
~scope:eref_scope
(fun () ->
let (session_group_string, send_session_group_string) :
(string React.E.t * (?step:React.step -> string -> unit) ) =
React.E.create ()
in
(session_group_string, send_session_group_string)
)
(* Reactive string to display the users session group size *)
let react_session_group_size_ref =
Eliom_reference.Volatile.eref_from_fun
~scope:eref_scope
(fun () ->
let (session_group_size_string, send_session_group_size_string) :
(string React.E.t * (?step:React.step -> string -> unit) ) =
React.E.create ()
in
(session_group_size_string, send_session_group_size_string)
)
(* Send a message from one client to another *)
let notify from_user_id to_user_id s =
(* Get the session group state for the user *)
let state =
Eliom_state.Ext.volatile_data_group_state ~scope:Eliom_common.default_group_scope to_user_id in
(* Iterate on all sessions from the group *)
Eliom_state.Ext.iter_volatile_sub_states ~state
(fun state ->
(* Iterate on all client process states in the session *)
Eliom_state.Ext.iter_volatile_sub_states ~state
(fun state ->
let (_, notify) = Eliom_reference.Volatile.Ext.get state channel_ref in
notify (Some ("Hello from " ^ from_user_id ^ "! You are user " ^ to_user_id ^ "\n\n" ^ s))
)
)
(* Action for a client to send a message *)
let () =
Eliom_registration.Action.register
~options:`NoReload
~service:new_message_action
(fun () (from_user_id, (to_user_id, msg)) ->
Lwt.return ## notify from_user_id to_user_id msg
)
(* Post form for one user to send a message to another user *)
let client_message_form =
Eliom_content.Html5.F.post_form ~service:new_message_action ~port:8080
(
fun (from_user_id, (to_user_id, msg)) ->
[p [pcdata "To:"];
string_input ~input_type:`Text ~name:to_user_id ();
p [pcdata "From:"];
string_input ~input_type:`Text ~name:from_user_id ();
p [pcdata "Send a message here:"];
string_input ~input_type:`Text ~name:msg ();
button ~button_type:`Submit [pcdata "Send Message"]
]
)
let () =
Channel_example_app.register
~service:main_service
(fun user_num () ->
(* Set the session group to which the erefs belong *)
Eliom_state.set_volatile_data_session_group
~set_max:1
~scope:Eliom_common.default_session_scope
~secure:true
user_num;
let (channel, _) = Eliom_reference.Volatile.get channel_ref in
let my_client_string, my_send_client_string = Eliom_reference.Volatile.get react_string_ref in
let my_send_client_string' =
server_function Json.t<string> (fun s -> Lwt.return ## my_send_client_string s)
in
let c_down = Eliom_react.Down.of_react my_client_string in
(* When a message is received on the channel, push it as a reactive event *)
let _ =
{unit{
Lwt.async
(fun () ->
Lwt_stream.iter (fun (s : string) -> ignore ## %my_send_client_string' s) %channel
)
}}
in
let my_session_group =
match
Eliom_state.get_volatile_data_session_group
~scope:Eliom_common.default_session_scope
~secure:true ()
with
| None -> "No session group"
| Some sg -> sg
in
let my_session_group_size =
match
Eliom_state.get_volatile_data_session_group_size
~scope:Eliom_common.default_session_scope
~secure:true ()
with
| None -> "0"
| Some gs -> string_of_int gs
in
Lwt.return
(Eliom_tools.F.html
~title:"channel_example"
~css:[["css";"channel_example.css"]]
Eliom_content.Html5.F.(body [
h2 [pcdata ("Your are logged in as user " ^ user_num)];
client_message_form ();
p [pcdata "Your message is:"];
C.node {{R.pcdata (React.S.hold "No message yet" %c_down)}};
p [pcdata ("I am a part of the session group named " ^ my_session_group)];
p [pcdata ("My session group size is " ^ my_session_group_size)]
])))
The problem came from using the notify function which loops through all tabs. I used the Hashtable / Weak Hashtable structure from Eliom Base App and it corrected all the communication issues. The key was altering the notify function as follows:
let notify ?(notforme = false) ~id ~to_user ~msg =
Lwt.async (fun () ->
I.fold
(fun (userid_o, ((_, _, send_e) as nn)) (beg : unit Lwt.t) ->
if notforme && nn == Eliom_reference.Volatile.get notif_e
then Lwt.return ()
else
lwt () = beg in
let content = if Some to_user = userid_o then Some msg else None in
match content with
| Some content -> send_e (id, content); Lwt.return ()
| None -> Lwt.return ()
)
id (Lwt.return ()))