User:Benmachine/New network package
This concept originally arose as "fix up the network package" to address the following perceived flaws:
- Timeout socket options are unusable because setSockOpt takes an Int but they want a struct timeval. This can't be worked around because the underlying C import is not exposed and anyway is imported with the wrong type.
- Only Network.URI uses parsec, and most users of network probably don't use that module, so the extra dependency is probably unnecessary.
- The strange behaviour of UnixSocket with connectTo and accept leads me to believe that perhaps the address datatypes aren't well thought-out, as the API design allows for nonsensical requests to be made.
- The API is conditionally exposed based on what symbols are or are not defined on the compilation platform: in a sense this is good because unavailable APIs are caught at compile time, but the error message you get in this case is awkward and has led to spurious bug reports and confusion. It also doesn't seem possible to account for these API differences without yourself using CPP, which seems clumsy to me.
- The PortNumber newtype defines a Num instance despite the fact that it really doesn't often make sense to subtract ports. It doesn't define a Read instance.
The last few points especially started to suggest that a change to the existing package would probably be quite sweeping and API-breaking, so the idea mutated into "design a new network package minus the above flaws", since that was likely to be less troublesome for upgraders.
2 New ideas
- Essentially a three-tiered API:
- the lowest-level FFI interface to the C API, exposing foreign imports and datatypes directly, along with their Storable instances, etc.
- something akin to the current Network package, that is organised to be equivalent in expressivity to the FFI interface but using more idiomatic types and signatures, and taking care of all the marshalling under the hood.
- nicer Haskell wrappers around the above that capture common or encouraged usage patterns. For example, many socket options only need to be set once, when the socket is created, so we might just add a [SocketOption] (or whatever) parameter to the socket creation function. This makes things less stateful and neater. Depending on how far we want to extend this idea, we could potentially make it a separate package so that we can take advantage of a wider range of dependencies (e.g. iteratee, safer-file-handles, etc.)
- Socket options:
- Instead of dispatching on a SocketOption ADT, use multiple functions: instead of we havesetSocketOption sock Debug 1or something similar.setSocketDebug sock True
- Or how about a class-based approach?
- Instead of dispatching on a SocketOption ADT, use multiple functions: instead of
newtype SocketDebug = SocketDebug Bool instance SocketOption SocketDebug where setSocketOption s (SocketDebug b) = c'setsockopt s (#const SO_DEBUG) (fromEnum b) -- or whatever getSocketOption s = c'getsockopt s somethingSillyWithMarshalling -- the idea is that the type, and therefore class instance, is inferred from use of the newtype constructor: twiddleDebug :: Socket -> IO () twiddleDebug s = do SocketDebug b <- getSocketOption s putStrLn ("Socket debug is: " ++ show b) setSocketOption s (SocketDebug True)
- Both of these are more extensible with "pseudo-options" than the ADT approach, which is nice. The latter may aid encapsulation of the Socket type:
<Twey> Say the SocketOption class contains methods to transform it to the ints C uses <Twey> Not without knowing the internal details of the Socket type <benmachine> ah but the C API uses void* <Twey> void*, whatever <benmachine> the point being that how you translate things to void* might depend a little on which socket option it is <Twey> Yeah <benmachine> and hence can't necessarily be done in a uniform way <Twey> Which is why it's in the class <benmachine> you mean in the instance methods? <Twey> Yes <benmachine> so you're suggesting that there's a function which takes a socket and a void* and does the FFI call <benmachine> and the class instances take care of accepting an Integer and turning it into a void* <Twey> Not necessarily an Integer <Twey> Whatever argument(s) they accept <benmachine> sure <benmachine> just an example <Twey> *nod*
- String and ByteString need to be given equal consideration. Whether this is done by module boundaries (as in Network.Socket.ByteString) or a type-class approach is still under discussion.
- I think that it encourages people to think about encoding and stuff if we use a concrete ByteString type everywhere. But maybe we want to let people not think about encoding sometimes? We don't want encoding-boilerplate all over the place.