Skip to content

Commit

Permalink
Monadstack tutorial
Browse files Browse the repository at this point in the history
  • Loading branch information
edsko committed Oct 25, 2024
1 parent 2c64092 commit 87dc7bb
Show file tree
Hide file tree
Showing 15 changed files with 1,294 additions and 1 deletion.
13 changes: 12 additions & 1 deletion .github/workflows/haskell-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ jobs:
if [ $((HCNUMVER < 91000)) -ne 0 ] ; then echo "packages: $GITHUB_WORKSPACE/source/./tutorials/basics" >> cabal.project ; fi
if [ $((HCNUMVER < 91000)) -ne 0 ] ; then echo "packages: $GITHUB_WORKSPACE/source/./tutorials/lowlevel" >> cabal.project ; fi
if [ $((HCNUMVER < 91000)) -ne 0 ] ; then echo "packages: $GITHUB_WORKSPACE/source/./tutorials/metadata" >> cabal.project ; fi
if [ $((HCNUMVER < 91000)) -ne 0 ] ; then echo "packages: $GITHUB_WORKSPACE/source/./tutorials/monadstack" >> cabal.project ; fi
cat cabal.project
- name: sdist
run: |
Expand All @@ -184,6 +185,8 @@ jobs:
echo "PKGDIR_lowlevel=${PKGDIR_lowlevel}" >> "$GITHUB_ENV"
PKGDIR_metadata="$(find "$GITHUB_WORKSPACE/unpacked" -maxdepth 1 -type d -regex '.*/metadata-[0-9.]*')"
echo "PKGDIR_metadata=${PKGDIR_metadata}" >> "$GITHUB_ENV"
PKGDIR_monadstack="$(find "$GITHUB_WORKSPACE/unpacked" -maxdepth 1 -type d -regex '.*/monadstack-[0-9.]*')"
echo "PKGDIR_monadstack=${PKGDIR_monadstack}" >> "$GITHUB_ENV"
rm -f cabal.project cabal.project.local
touch cabal.project
touch cabal.project.local
Expand All @@ -193,6 +196,7 @@ jobs:
if [ $((HCNUMVER < 91000)) -ne 0 ] ; then echo "packages: ${PKGDIR_basics}" >> cabal.project ; fi
if [ $((HCNUMVER < 91000)) -ne 0 ] ; then echo "packages: ${PKGDIR_lowlevel}" >> cabal.project ; fi
if [ $((HCNUMVER < 91000)) -ne 0 ] ; then echo "packages: ${PKGDIR_metadata}" >> cabal.project ; fi
if [ $((HCNUMVER < 91000)) -ne 0 ] ; then echo "packages: ${PKGDIR_monadstack}" >> cabal.project ; fi
echo "package grpc-spec" >> cabal.project
echo " ghc-options: -Werror=missing-methods" >> cabal.project
echo "package grapesy" >> cabal.project
Expand All @@ -205,6 +209,8 @@ jobs:
if [ $((HCNUMVER < 91000)) -ne 0 ] ; then echo " ghc-options: -Werror=missing-methods" >> cabal.project ; fi
if [ $((HCNUMVER < 91000)) -ne 0 ] ; then echo "package metadata" >> cabal.project ; fi
if [ $((HCNUMVER < 91000)) -ne 0 ] ; then echo " ghc-options: -Werror=missing-methods" >> cabal.project ; fi
if [ $((HCNUMVER < 91000)) -ne 0 ] ; then echo "package monadstack" >> cabal.project ; fi
if [ $((HCNUMVER < 91000)) -ne 0 ] ; then echo " ghc-options: -Werror=missing-methods" >> cabal.project ; fi
cat >> cabal.project <<EOF
allow-newer: proto-lens:base
allow-newer: proto-lens-runtime:base
Expand All @@ -231,8 +237,11 @@ jobs:
package metadata
ghc-options: -Werror
package monadstack
ghc-options: -Werror
EOF
$HCPKG list --simple-output --names-only | perl -ne 'for (split /\s+/) { print "constraints: any.$_ installed\n" unless /^(basics|grapesy|grpc-spec|lowlevel|metadata|quickstart)$/; }' >> cabal.project.local
$HCPKG list --simple-output --names-only | perl -ne 'for (split /\s+/) { print "constraints: any.$_ installed\n" unless /^(basics|grapesy|grpc-spec|lowlevel|metadata|monadstack|quickstart)$/; }' >> cabal.project.local
cat cabal.project
cat cabal.project.local
- name: dump install plan
Expand Down Expand Up @@ -272,6 +281,8 @@ jobs:
if [ $((HCNUMVER < 91000)) -ne 0 ] ; then ${CABAL} -vnormal check ; fi
if [ $((HCNUMVER < 91000)) -ne 0 ] ; then cd ${PKGDIR_metadata} || false ; fi
if [ $((HCNUMVER < 91000)) -ne 0 ] ; then ${CABAL} -vnormal check ; fi
if [ $((HCNUMVER < 91000)) -ne 0 ] ; then cd ${PKGDIR_monadstack} || false ; fi
if [ $((HCNUMVER < 91000)) -ne 0 ] ; then ${CABAL} -vnormal check ; fi
- name: haddock
run: |
$CABAL v2-haddock --disable-documentation $ARG_COMPILER --with-haddock $HADDOCK $ARG_TESTS $ARG_BENCH all
Expand Down
1 change: 1 addition & 0 deletions cabal.project
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ packages:
, ./tutorials/basics
, ./tutorials/lowlevel
, ./tutorials/metadata
, ./tutorials/monadstack

package grpc-spec
tests: True
Expand Down
4 changes: 4 additions & 0 deletions cabal.project.ci
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ packages:
, ./tutorials/basics
, ./tutorials/lowlevel
, ./tutorials/metadata
, ./tutorials/monadstack

package grpc-spec
tests: True
Expand All @@ -29,6 +30,9 @@ package lowlevel
package metadata
ghc-options: -Werror

package monadstack
ghc-options: -Werror

--
-- ghc 9.10
--
Expand Down
31 changes: 31 additions & 0 deletions tutorials/monadstack/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
Copyright (c) 2023-2024, Well-Typed LLP and Anduril Industries Inc.

All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.

* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided
with the distribution.

* Neither the name of Well-Typed LLP, the name of Anduril
Industries Inc., nor the names of other contributors may be
used to endorse or promote products derived from this software
without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
7 changes: 7 additions & 0 deletions tutorials/monadstack/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Basics tutorial

See `/tutorials/basics` for the more direct `grapesy` translation of the
[official Basics tutorial](https://grpc.io/docs/languages/python/basics/).

In this tutorial we re-implement both the server and the client using a
custom monad stack.
3 changes: 3 additions & 0 deletions tutorials/monadstack/Setup.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import Data.ProtoLens.Setup

main = defaultMainGeneratingProtos "proto"
137 changes: 137 additions & 0 deletions tutorials/monadstack/app/Client.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
module Client (main) where

import Control.Concurrent
import Control.Monad
import Control.Monad.Catch
import Control.Monad.Reader
import Data.Int
import Data.Text (Text)
import System.Random

import Network.GRPC.Client
import Network.GRPC.Client.StreamType.CanCallRPC
import Network.GRPC.Common
import Network.GRPC.Common.NextElem qualified as NextElem
import Network.GRPC.Common.Protobuf

import RouteGuide

import Proto.API.RouteGuide

{-------------------------------------------------------------------------------
Custom client monad
The only requirement here as far as @grapesy@ is concerned is that
-------------------------------------------------------------------------------}

newtype Client a = WrapClient {
unwrapClient :: ReaderT ClientEnv IO a
}
deriving newtype (
Functor
, Applicative
, Monad
, MonadIO
, MonadCatch
, MonadThrow
, MonadMask
)

data ClientEnv = ClientEnv {
conn :: Connection
}

runClient :: Connection -> Client a -> IO a
runClient conn = flip runReaderT ClientEnv{conn} . unwrapClient

instance CanCallRPC Client where
getConnection = WrapClient $ conn <$> ask

{-------------------------------------------------------------------------------
Call each of the methods of the RouteGuide service
-------------------------------------------------------------------------------}

getFeature :: Client ()
getFeature = do
let req = defMessage
& #latitude .~ 409146138
& #longitude .~ -746188906
resp <- nonStreaming (rpc @(Protobuf RouteGuide "getFeature")) req
liftIO $ print resp

listFeatures :: Client ()
listFeatures = do
let lo = defMessage
& #latitude .~ 400000000
& #longitude .~ -750000000
hi = defMessage
& #latitude .~ 420000000
& #longitude .~ -730000000
req = defMessage
& #lo .~ lo
& #hi .~ hi
serverStreaming (rpc @(Protobuf RouteGuide "listFeatures")) req $ \recv -> liftIO $
NextElem.whileNext_ recv print

recordRoute :: Client ()
recordRoute = do
db <- liftIO $ getDB
resp <- clientStreaming_ (rpc @(Protobuf RouteGuide "recordRoute")) $ \send -> liftIO $ do
replicateM_ 10 $ do
i <- randomRIO (0, length db - 1)
let p = (db !! i) ^. #location
send $ NextElem p
threadDelay 500_000 -- 0.5 seconds
send NoNextElem
liftIO $ print resp

routeChat :: Client ()
routeChat = do
biDiStreaming (rpc @(Protobuf RouteGuide "routeChat")) $ \send recv -> liftIO $ do
NextElem.forM_ messages send
NextElem.whileNext_ recv print
where
messages :: [Proto RouteNote]
messages = [
makeRouteNote "First message" 0 0
, makeRouteNote "Second message" 0 1
, makeRouteNote "Third message" 1 0
, makeRouteNote "Fourth message" 0 0
, makeRouteNote "Fifth message" 1 0
]

makeRouteNote :: Text -> Int32 -> Int32 -> Proto RouteNote
makeRouteNote message latitude longitude =
let location =
defMessage
& #latitude .~ latitude
& #longitude .~ longitude
in defMessage
& #message .~ message
& #location .~ location

{-------------------------------------------------------------------------------
Main application
-------------------------------------------------------------------------------}

client :: Client ()
client = do
liftIO $ putStrLn "-------------- GetFeature --------------"
getFeature

liftIO $ putStrLn "-------------- ListFeatures --------------"
listFeatures

liftIO $ putStrLn "-------------- RecordRoute --------------"
recordRoute

liftIO $ putStrLn "-------------- RouteChat --------------"
routeChat

main :: IO ()
main =
withConnection def server $ \conn ->
runClient conn client
where
server :: Server
server = ServerInsecure $ Address "127.0.0.1" defaultInsecurePort Nothing
100 changes: 100 additions & 0 deletions tutorials/monadstack/app/Server.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
module Server (main) where

import Control.Monad
import Control.Monad.Reader
import Data.IORef
import Data.Maybe (fromMaybe)
import Data.Time

import Network.GRPC.Common
import Network.GRPC.Common.Protobuf
import Network.GRPC.Server.Protobuf
import Network.GRPC.Server.Run
import Network.GRPC.Server.StreamType
import Network.GRPC.Common.NextElem qualified as NextElem

import Proto.API.RouteGuide

import RouteGuide

{-------------------------------------------------------------------------------
Custom monad for handlers
The only requirement here as far as @grapesy@ is concerned is that we can
ultimately hoist these into @IO@ ('hoistMethods').
-------------------------------------------------------------------------------}

newtype Handler a = WrapHandler {
unwrapHandler :: ReaderT DB IO a
}
deriving newtype (Functor, Applicative, Monad, MonadIO, MonadReader DB)

runHandler :: DB -> Handler a -> IO a
runHandler db = flip runReaderT db . unwrapHandler

{-------------------------------------------------------------------------------
Individual handlers
-------------------------------------------------------------------------------}

getFeature :: Proto Point -> Handler (Proto Feature)
getFeature p = do
db <- ask
return $ fromMaybe (defMessage & #location .~ p) (featureAt db p)

listFeatures ::
Proto Rectangle
-> (NextElem (Proto Feature) -> IO ())
-> Handler ()
listFeatures r send = do
db <- ask
liftIO $ NextElem.forM_ (featuresIn db r) send

recordRoute ::
IO (NextElem (Proto Point))
-> Handler (Proto RouteSummary)
recordRoute recv = do
db <- ask
liftIO $ do
start <- getCurrentTime
ps <- NextElem.collect recv
stop <- getCurrentTime
return $ summary db (stop `diffUTCTime` start) ps

routeChat ::
IO (NextElem (Proto RouteNote))
-> (NextElem (Proto RouteNote) -> IO ())
-> Handler ()
routeChat recv send = liftIO $ do
st :: IORef Chat <- newIORef emptyChat
NextElem.whileNext_ recv $ \note -> do
prev <- atomicModifyIORef st $ \chat -> (
recordNote note chat
, getNotes chat (note ^. #location)
)
-- Can't use NextElem.forM_ here: we don't want to send 'NoNextElem' yet
forM_ prev $ send . NextElem
send NoNextElem

{-------------------------------------------------------------------------------
Server top-level
-------------------------------------------------------------------------------}

methods :: Methods Handler (ProtobufMethodsOf RouteGuide)
methods =
Method (mkNonStreaming getFeature )
$ Method (mkServerStreaming listFeatures)
$ Method (mkClientStreaming recordRoute )
$ Method (mkBiDiStreaming routeChat )
$ NoMoreMethods

main :: IO ()
main = do
db <- getDB
runServerWithHandlers def config $ fromMethods $
hoistMethods (runHandler db) methods
where
config :: ServerConfig
config = ServerConfig {
serverInsecure = Just (InsecureConfig Nothing defaultInsecurePort)
, serverSecure = Nothing
}
4 changes: 4 additions & 0 deletions tutorials/monadstack/data/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# RouteGuide features database

This is a copy of the database from the official gRPC repo at
http://github.com/grpc/grpc.
Loading

0 comments on commit 87dc7bb

Please sign in to comment.