2023-12-25に更新

Haskellを練習しようぜ(^o^)?<その2o0>

読了目安:25分

前の関連記事

📖 Haskellを練習しようぜ(^o^)?<その1o0>

情報

📖 haskell-practice-on-ubuntu

はじめに

ramen-tabero-futsu2.png
「 エコー・サーバーのサンプル・プログラム無い?」

📖 Implement a chat server

ohkina-hiyoko-futsu2.png
「 ↑ チャット・サーバーなら すぐ見つかったけど」

ramen-tabero-futsu2.png
「 じゃあ それを真似よう」

cabal init --interactive
muzudho@muzudho-MS-7B09:~/Documents/git_hub/haskell-practice-on-ubuntu$ cabal init --interactive
Should I generate a simple project with sensible defaults? [default: y] n
What does the package build:
   1) Executable
   2) Library
   3) Library and Executable
Your choice? 1
What is the main module of the executable:
 * 1) Main.hs (does not yet exist, but will be created)
   2) Main.lhs (does not yet exist, but will be created)
   3) Other (specify)
Your choice? [default: Main.hs (does not yet exist, but will be created)] 1
Please choose version of the Cabal specification to use:
   1) 1.10   (legacy)
   2) 2.0    (+ support for Backpack, internal sub-libs, '^>=' operator)
   3) 2.2    (+ support for 'common', 'elif', redundant commas, SPDX)
 * 4) 2.4    (+ support for '**' globbing)
   5) 3.0    (+ set notation for ==, common stanzas in ifs, more redundant commas, better pkgconfig-depends)
Your choice? [default: 2.4    (+ support for '**' globbing)] 
Package name? [default: haskell-practice-on-ubuntu] chat_server
Couldn't parse chat_server, please try again!
Package name? [default: haskell-practice-on-ubuntu] chat-server
Package version? [default: 0.1.0.0] 
Please choose a license:
 * 1) NONE
   2) BSD-2-Clause
   3) BSD-3-Clause
   4) Apache-2.0
   5) MIT
   6) MPL-2.0
   7) ISC
   8) GPL-2.0-only
   9) GPL-3.0-only
  10) LGPL-2.1-only
  11) LGPL-3.0-only
  12) AGPL-3.0-only
  13) GPL-2.0-or-later
  14) GPL-3.0-or-later
  15) LGPL-2.1-or-later
  16) LGPL-3.0-or-later
  17) AGPL-3.0-or-later
  18) Other (specify)
Your choice? [default: NONE] 5
Author name? [default: muzudho] 
Maintainer email? [default: [email protected]] 
Project homepage URL? https://github.com/muzudho/haskell-practice-on-ubuntu
Project synopsis? for shogi server                                
Project category:
 * 1) (none)
   2) Codec
   3) Concurrency
   4) Control
   5) Data
   6) Database
   7) Development
   8) Distribution
   9) Game
  10) Graphics
  11) Language
  12) Math
  13) Network
  14) Sound
  15) System
  16) Testing
  17) Text
  18) Web
  19) Other (specify)
Your choice? [default: (none)] 13
What base language is the package written in:
 * 1) Haskell2010
   2) Haskell98
   3) Other (specify)
Your choice? [default: Haskell2010] 
Add informative comments to each field in the cabal file (y/n)? [default: n] 

Guessing dependencies...

Generating LICENSE...
Warning: LICENSE already exists, backing up old version in LICENSE.save0
Generating CHANGELOG.md...
Warning: CHANGELOG.md already exists, backing up old version in CHANGELOG.md.save0
Generating chat-server.cabal...

You may want to edit the .cabal file and add a Description field.

ramen-tabero-futsu2.png
「 しまった プロジェクト・フォルダーの中身が ぶちまけられて ファイルをリネームまで されてしまった、
手動で復元しよ」

step 1o0

🗒 Main.hs :

-- in Main.hs
module Main where

import Network.Socket

main :: IO ()
main = do
    sock <- socket AF_INET Stream 0    -- create socket
    setSocketOption sock ReuseAddr 1   -- make socket immediately reusable - eases debugging.
    bind sock (SockAddrInet 4242 iNADDR_ANY)   -- listen on TCP port 4242.
    listen sock 2                              -- set a max of 2 queued connections
    mainLoop sock                              -- unimplemented

Output:

muzudho@muzudho-MS-7B09:~/Documents/git_hub/haskell-practice-on-ubuntu/chat-server$ cabal build
Build profile: -w ghc-9.4.7 -O1
In order, the following will be built (use -v for more details):
 - chat-server-0.1.0.0 (exe:chat-server) (file app/Main.hs changed)
Preprocessing executable 'chat-server' for chat-server-0.1.0.0..
Building executable 'chat-server' for chat-server-0.1.0.0..
[1 of 1] Compiling Main             ( app/Main.hs, /home/muzudho/Documents/git_hub/haskell-practice-on-ubuntu/chat-server/dist-newstyle/build/x86_64-linux/ghc-9.4.7/chat-server-0.1.0.0/x/chat-server/build/chat-server/chat-server-tmp/Main.o ) [Source file changed]

app/Main.hs:4:1: error:
    Could not find module ‘Network.Socket’
    Use -v (or `:set -v` in ghci) to see a list of the files searched for.
  |
4 | import Network.Socket
  | ^^^^^^^^^^^^^^^^^^^^^

ramen-tabero-futsu2.png
「 ↑ 依存性は .cabal ファイルにどう書けばいいんだ?」

executable chat-server
    -- ...
    build-depends:    
        base ^>=4.17.2.0,
        network,
        chat-server

ramen-tabero-futsu2.png
「 ↑ network を足してみるか」

muzudho@muzudho-MS-7B09:~/Documents/git_hub/haskell-practice-on-ubuntu/chat-server$ cabal build

...

app/Main.hs:10:34: error:
    Variable not in scope: iNADDR_ANY :: HostAddress
   |
10 |     bind sock (SockAddrInet 4242 iNADDR_ANY)   -- listen on TCP port 4242.
   |                                  ^^^^^^^^^^

app/Main.hs:12:5: error:
    Variable not in scope: mainLoop :: Socket -> IO ()
   |
12 |     mainLoop sock                              -- unimplemented
   |     ^^^^^^^^

ramen-tabero-futsu2.png
「 ↑ このエラーはサンプル通りかな」

ohkina-hiyoko-futsu2.png
「 動かないコードをサンプルにするのは 止めてほしいわねぇ」

step 2o0

-- in Main.hs
module Main where

import Network.Socket

-- メイン
main :: IO ()
main = do
    sock <- socket AF_INET Stream 0    -- create socket
    setSocketOption sock ReuseAddr 1   -- make socket immediately reusable - eases debugging.
    bind sock (SockAddrInet 4242 iNADDR_ANY)   -- listen on TCP port 4242.
    listen sock 2                              -- set a max of 2 queued connections
    mainLoop sock                              -- unimplemented

-- メインループ
mainLoop :: Socket -> IO ()
mainLoop sock = do
    conn <- accept sock     -- accept a connection and handle it
    runConn conn            -- run our server's logic
    mainLoop sock           -- repeat

runConn :: (Socket, SockAddr) -> IO ()
runConn (sock, _) = do
    send sock "Hello!\n"
    close sock

Output:

muzudho@muzudho-MS-7B09:~/Documents/git_hub/haskell-practice-on-ubuntu/chat-server$ cabal build
Build profile: -w ghc-9.4.7 -O1
In order, the following will be built (use -v for more details):
 - chat-server-0.1.0.0 (exe:chat-server) (file app/Main.hs changed)
Preprocessing executable 'chat-server' for chat-server-0.1.0.0..
Building executable 'chat-server' for chat-server-0.1.0.0..
[1 of 1] Compiling Main             ( app/Main.hs, /home/muzudho/Documents/git_hub/haskell-practice-on-ubuntu/chat-server/dist-newstyle/build/x86_64-linux/ghc-9.4.7/chat-server-0.1.0.0/x/chat-server/build/chat-server/chat-server-tmp/Main.o ) [Source file changed]

app/Main.hs:11:34: error:
    Variable not in scope: iNADDR_ANY :: HostAddress
   |
11 |     bind sock (SockAddrInet 4242 iNADDR_ANY)   -- listen on TCP port 4242.
   |                                  ^^^^^^^^^^

app/Main.hs:24:5: error:
    Variable not in scope: send :: Socket -> String -> IO a0
    Suggested fix: Perhaps use ‘snd’ (imported from Prelude)
   |
24 |     send sock "Hello!\n"
   |     ^^^^

ramen-tabero-futsu2.png
「 ↑ 少しずつ説明していくのだろう、それも どうかと思うが」

step 3o0

-- in Main.hs
module Main where

import Network.Socket

-- in the imports our Main.hs add:
import System.IO

-- メイン
main :: IO ()
main = do
    sock <- socket AF_INET Stream 0    -- create socket
    setSocketOption sock ReuseAddr 1   -- make socket immediately reusable - eases debugging.
    bind sock (SockAddrInet 4242 iNADDR_ANY)   -- listen on TCP port 4242.
    listen sock 2                              -- set a max of 2 queued connections
    mainLoop sock                              -- unimplemented

-- メインループ
mainLoop :: Socket -> IO ()
mainLoop sock = do
    conn <- accept sock     -- accept a connection and handle it
    runConn conn            -- run our server's logic
    mainLoop sock           -- repeat

-- and we'll change our `runConn` function to look like:
runConn :: (Socket, SockAddr) -> IO ()
runConn (sock, _) = do
    hdl <- socketToHandle sock ReadWriteMode
    hSetBuffering hdl NoBuffering
    hPutStrLn hdl "Hello!"
    hClose hdl
muzudho@muzudho-MS-7B09:~/Documents/git_hub/haskell-practice-on-ubuntu/chat-server$ cabal build
Build profile: -w ghc-9.4.7 -O1
In order, the following will be built (use -v for more details):
 - chat-server-0.1.0.0 (exe:chat-server) (file app/Main.hs changed)
Preprocessing executable 'chat-server' for chat-server-0.1.0.0..
Building executable 'chat-server' for chat-server-0.1.0.0..
[1 of 1] Compiling Main             ( app/Main.hs, /home/muzudho/Documents/git_hub/haskell-practice-on-ubuntu/chat-server/dist-newstyle/build/x86_64-linux/ghc-9.4.7/chat-server-0.1.0.0/x/chat-server/build/chat-server/chat-server-tmp/Main.o ) [Source file changed]

app/Main.hs:14:34: error:
    Variable not in scope: iNADDR_ANY :: HostAddress
   |
14 |     bind sock (SockAddrInet 4242 iNADDR_ANY)   -- listen on TCP port 4242.
   |                                  ^^^^^^^^^^

ramen-tabero-futsu2.png
「 ↑ おまじないで 丸覚えしていくかあ」

step 4o0

-- in Main.hs
module Main where

import Network.Socket

-- in the imports our Main.hs add:
import System.IO

-- add to our imports:
import Control.Concurrent

-- メイン
main :: IO ()
main = do
    sock <- socket AF_INET Stream 0    -- create socket
    setSocketOption sock ReuseAddr 1   -- make socket immediately reusable - eases debugging.
    bind sock (SockAddrInet 4242 iNADDR_ANY)   -- listen on TCP port 4242.
    listen sock 2                              -- set a max of 2 queued connections
    mainLoop sock                              -- unimplemented

-- メインループ
mainLoop :: Socket -> IO ()
mainLoop sock = do
    conn <- accept sock     -- accept a connection and handle it
    forkIO (runConn conn)   -- run our server's logic. split off each connection into its own thread
    mainLoop sock           -- repeat

-- and we'll change our `runConn` function to look like:
runConn :: (Socket, SockAddr) -> IO ()
runConn (sock, _) = do
    hdl <- socketToHandle sock ReadWriteMode
    hSetBuffering hdl NoBuffering
    hPutStrLn hdl "Hello!"
    hClose hdl
muzudho@muzudho-MS-7B09:~/Documents/git_hub/haskell-practice-on-ubuntu/chat-server$ cabal build
Build profile: -w ghc-9.4.7 -O1
In order, the following will be built (use -v for more details):
 - chat-server-0.1.0.0 (exe:chat-server) (file app/Main.hs changed)
Preprocessing executable 'chat-server' for chat-server-0.1.0.0..
Building executable 'chat-server' for chat-server-0.1.0.0..
[1 of 1] Compiling Main             ( app/Main.hs, /home/muzudho/Documents/git_hub/haskell-practice-on-ubuntu/chat-server/dist-newstyle/build/x86_64-linux/ghc-9.4.7/chat-server-0.1.0.0/x/chat-server/build/chat-server/chat-server-tmp/Main.o ) [Source file changed]

app/Main.hs:17:34: error:
    Variable not in scope: iNADDR_ANY :: HostAddress
   |
17 |     bind sock (SockAddrInet 4242 iNADDR_ANY)   -- listen on TCP port 4242.
   |       

ramen-tabero-futsu2.png
「 ↑ 試しながら 覚えていくわけではないので なんも身につかん。座学」

step 5o0

-- in Main.hs
module Main where

import Network.Socket

-- in the imports our Main.hs add:
import System.IO

-- add to our imports:
import Control.Concurrent

-- in Main.hs
type Msg = String

-- メイン
main :: IO ()
main = do
    sock <- socket AF_INET Stream 0    -- create socket
    setSocketOption sock ReuseAddr 1   -- make socket immediately reusable - eases debugging.
    bind sock (SockAddrInet 4242 iNADDR_ANY)   -- listen on TCP port 4242.
    listen sock 2                              -- set a max of 2 queued connections
    chan <- newChan        -- notice that newChan :: IO (Chan a)
    mainLoop sock chan     -- pass it into the loop

-- メインループ
mainLoop :: Socket -> Chan Msg -> IO ()   -- See how Chan now uses Msg.
mainLoop sock chan = do
    conn <- accept sock         -- accept a connection and handle it
    forkIO (runConn conn chan)  -- run our server's logic. split off each connection into its own thread. pass the channel to runConn
    mainLoop sock chan          -- repeat

-- and we'll change our `runConn` function to look like:
runConn :: (Socket, SockAddr) -> IO ()
runConn (sock, _) = do
    hdl <- socketToHandle sock ReadWriteMode
    hSetBuffering hdl NoBuffering
    hPutStrLn hdl "Hello!"
    hClose hdl
muzudho@muzudho-MS-7B09:~/Documents/git_hub/haskell-practice-on-ubuntu/chat-server$ cabal build
Build profile: -w ghc-9.4.7 -O1
In order, the following will be built (use -v for more details):
 - chat-server-0.1.0.0 (exe:chat-server) (file app/Main.hs changed)
Preprocessing executable 'chat-server' for chat-server-0.1.0.0..
Building executable 'chat-server' for chat-server-0.1.0.0..
[1 of 1] Compiling Main             ( app/Main.hs, /home/muzudho/Documents/git_hub/haskell-practice-on-ubuntu/chat-server/dist-newstyle/build/x86_64-linux/ghc-9.4.7/chat-server-0.1.0.0/x/chat-server/build/chat-server/chat-server-tmp/Main.o ) [Source file changed]

app/Main.hs:20:34: error:
    Variable not in scope: iNADDR_ANY :: HostAddress
   |
20 |     bind sock (SockAddrInet 4242 iNADDR_ANY)   -- listen on TCP port 4242.
   |                                  ^^^^^^^^^^

ramen-tabero-futsu2.png
「 ↑ なんか Chan というのを使うようだが さっぱり 意味ワカラン」

step 6o0

app/Main.hs:16:1: error: parse error on input ‘import’
   |
16 | import Control.Monad.Fix (fix)
   | ^^^^^^

ramen-tabero-futsu2.png
「 ↑ うーん、エラー、依存性かな」

ramen-tabero-futsu2.png
「 違うや、 import 文は type 文より先に書かないといけないみたいだ」

-- in Main.hs
module Main where

import Network.Socket

-- in the imports our Main.hs add:
import System.IO

-- add to our imports:
import Control.Concurrent

-- at the top of Main.hs
import Control.Monad.Fix (fix)

-- in Main.hs
type Msg = String

-- メイン
main :: IO ()
main = do
    sock <- socket AF_INET Stream 0    -- create socket
    setSocketOption sock ReuseAddr 1   -- make socket immediately reusable - eases debugging.
    bind sock (SockAddrInet 4242 iNADDR_ANY)   -- listen on TCP port 4242.
    listen sock 2                              -- set a max of 2 queued connections
    chan <- newChan        -- notice that newChan :: IO (Chan a)
    mainLoop sock chan     -- pass it into the loop

-- メインループ
mainLoop :: Socket -> Chan Msg -> IO ()   -- See how Chan now uses Msg.
mainLoop sock chan = do
    conn <- accept sock         -- accept a connection and handle it
    forkIO (runConn conn chan)  -- run our server's logic. split off each connection into its own thread. pass the channel to runConn
    mainLoop sock chan          -- repeat

-- and we'll change our `runConn` function to look like:
runConn :: (Socket, SockAddr) -> Chan Msg -> IO ()
runConn (sock, _) chan = do
    let broadcast msg = writeChan chan msg
    hdl <- socketToHandle sock ReadWriteMode
    hSetBuffering hdl NoBuffering
    commLine <- dupChan chan

    -- fork off a thread for reading from the duplicated channel
    forkIO $ fix $ \loop -> do
        line <- readChan commLine
        hPutStrLn hdl line
        loop

    -- read lines from the socket and echo them back to the user
    fix $ \loop -> do
        line <- fmap init (hGetLine hdl) 
        broadcast line
        loop
muzudho@muzudho-MS-7B09:~/Documents/git_hub/haskell-practice-on-ubuntu/chat-server$ cabal build
Build profile: -w ghc-9.4.7 -O1
In order, the following will be built (use -v for more details):
 - chat-server-0.1.0.0 (exe:chat-server) (file app/Main.hs changed)
Preprocessing executable 'chat-server' for chat-server-0.1.0.0..
Building executable 'chat-server' for chat-server-0.1.0.0..
[1 of 1] Compiling Main             ( app/Main.hs, /home/muzudho/Documents/git_hub/haskell-practice-on-ubuntu/chat-server/dist-newstyle/build/x86_64-linux/ghc-9.4.7/chat-server-0.1.0.0/x/chat-server/build/chat-server/chat-server-tmp/Main.o ) [Source file changed]

app/Main.hs:23:34: error:
    Variable not in scope: iNADDR_ANY :: HostAddress
   |
23 |     bind sock (SockAddrInet 4242 iNADDR_ANY)   -- listen on TCP port 4242.
   |                                  ^^^^^^^^^^

ramen-tabero-futsu2.png
「 ↑ ビルドの通らないコードを書かされるの 続く」

step 7o0

-- Main.hs, final code
module Main where

import Network.Socket
import System.IO
import Control.Exception
import Control.Concurrent
import Control.Monad (when)
import Control.Monad.Fix (fix)


-- メイン
main :: IO ()
main = do
    sock <- socket AF_INET Stream 0    -- create socket
    setSocketOption sock ReuseAddr 1   -- make socket immediately reusable - eases debugging.
    bind sock (SockAddrInet 4242 iNADDR_ANY)   -- listen on TCP port 4242.
    listen sock 2                              -- set a max of 2 queued connections
    chan <- newChan                     -- notice that newChan :: IO (Chan a)
    _ <- forkIO $ fix $ \loop -> do
        (_, _) <- readChan chan
        loop
    mainLoop sock chan 0                -- pass it into the loop


type Msg = (Int, String)


-- メインループ
mainLoop :: Socket -> Chan Msg -> Int -> IO ()   -- See how Chan now uses Msg.
mainLoop sock chan msgNum = do
    conn <- accept sock                 -- accept a connection and handle it
    forkIO (runConn conn chan msgNum)   -- run our server's logic. split off each connection into its own thread. pass the channel to runConn
    mainLoop sock chan $! msgNum + 1    -- repeat

-- and we'll change our `runConn` function to look like:
runConn :: (Socket, SockAddr) -> Chan Msg -> Int -> IO ()
runConn (sock, _) chan msgNum = do
    let broadcast msg = writeChan chan (msgNum, msg)
    hdl <- socketToHandle sock ReadWriteMode
    hSetBuffering hdl NoBuffering

    hPutStrLn hdl "Hi, what's your name?"
    name <- fmap init (hGetLine hdl)
    broadcast ("--> " ++ name ++ " entered chat.")
    hPutStrLn hdl ("Welcome, " ++ name ++ "!")


    commLine <- dupChan chan

    -- fork off a thread for reading from the duplicated channel
    reader <- forkIO $ fix $ \loop -> do
        (nextNum, line) <- readChan commLine
        when (msgNum /= nextNum) $ hPutStrLn hdl line
        loop

    handle (\(SomeException _) -> return ()) $ fix $ \loop -> do
        line <- fmap init (hGetLine hdl)
        case line of
             -- If an exception is caught, send a message and break the loop
             "quit" -> hPutStrLn hdl "Bye!"
             -- else, continue looping.
             _      -> broadcast (name ++ ": " ++ line) >> loop

    killThread reader                      -- kill after the loop ends
    broadcast ("<-- " ++ name ++ " left.") -- make a final broadcast
    hClose hdl                             -- close the handle
muzudho@muzudho-MS-7B09:~/Documents/git_hub/haskell-practice-on-ubuntu/chat-server$ cabal build
Build profile: -w ghc-9.4.7 -O1
In order, the following will be built (use -v for more details):
 - chat-server-0.1.0.0 (exe:chat-server) (file app/Main.hs changed)
Preprocessing executable 'chat-server' for chat-server-0.1.0.0..
Building executable 'chat-server' for chat-server-0.1.0.0..
[1 of 1] Compiling Main             ( app/Main.hs, /home/muzudho/Documents/git_hub/haskell-practice-on-ubuntu/chat-server/dist-newstyle/build/x86_64-linux/ghc-9.4.7/chat-server-0.1.0.0/x/chat-server/build/chat-server/chat-server-tmp/Main.o ) [Source file changed]

app/Main.hs:17:34: error:
    Variable not in scope: iNADDR_ANY :: HostAddress
   |
17 |     bind sock (SockAddrInet 4242 iNADDR_ANY)   -- listen on TCP port 4242.
   |     

ramen-tabero-futsu2.png
「 ↑ これが サンプルの完成形だが、エラーが出ている」

kifuwarabe-futsu.png
「 わらう」

ohkina-hiyoko-futsu2.png
「 どういうエラーなの?」

ramen-tabero-futsu2.png
「 さっぱりワカラン」

📖 Network.Socket

ramen-tabero-futsu2.png
「 ↑ Network.Socket にありそうなもんだけどな」

ramen-tabero-futsu2.png
「 定数で 0 の意味らしいから iNADDR_ANY は 0 に置き換えたろ」

kifuwarabe-futsu.png
「 雑な仕事 わらう」

ramen-tabero-futsu2.png
「 ビルドは通った」

接続

cabal run

ramen-tabero-futsu2.png
「 ↑ これで サーバーは立ったのか?」

$ telnet localhost 4242

Output:

Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
Hi, what's your name?

ramen-tabero-futsu2.png
「 ↑ なんか 応答が返ってきたぜ」

muzudho
Welcome, muzudho!
quit
Bye!
Connection closed by foreign host.

ramen-tabero-futsu2.png
「 ↑ じゃあ このプログラムを改造すれば クライアント・サーバー型の通信ができるんだ」

kifuwarabe-futsu.png
「 また来年だな」

.

ツイッターでシェア
みんなに共有、忘れないようにメモ

むずでょ

光速のアカウント凍結されちゃったんで……。ゲームプログラムを独習中なんだぜ☆電王戦IIに出た棋士もコンピューターもみんな好きだぜ☆▲(パソコン将棋)WCSC29一次予選36位、SDT5予選42位▲(パソコン囲碁)AI竜星戦予選16位

Crieitは誰でも投稿できるサービスです。 是非記事の投稿をお願いします。どんな軽い内容でも投稿できます。

また、「こんな記事が読みたいけど見つからない!」という方は是非記事投稿リクエストボードへ!

有料記事を販売できるようになりました!

こじんまりと作業ログやメモ、進捗を書き残しておきたい方はボード機能をご利用ください。
ボードとは?

コメント