ทำ sha1-challenge ด้วย Haskell
Tags: haskell
Published: 2021-02-25 14:34:26

ไปเจอเพื่อนร่วมงานเล่นโจทย์ https://github.com/mrchoke/sha1-challages เลยลองเล่นดูบ้างโดยเขียนด้วย Haskell

โจทย์มีอยู่ว่า

> เริ่มต้นด้วยคำว่า "clubhouse"

> นำไป sha1 ครั้งแรกจะได้ "1313994e55ed4bbe79d2b04e4529ee2f4ac288f5"

> นำคำตอบไป sha1 ครั้งที่ 2 จะได้ "b42f871cff37138a6b8f53cf55a585d7d1445dfb"

> นำคำตอบไป sha1 ครั้งที่ 3 จะได้ "5c7c81615ad8050363c911c7b5e5ed8661be80d6"

> ทำไปเรื่อยๆ จนถึงครั้งที่ 5,555,555,555 จะได้ sha1 ลงท้ายด้วย "229a"

สำหรับ Haskell ผมใช้เครื่องมือในการเริ่มโปรเจ็คคือ [stack](https://docs.haskellstack.org/en/stable/README/) ติดตั้งง่ายๆได้ทั้งบน macOS, Linux และ Windows จากนั้นก็เริ่มสร้างโปรเจ็คด้วยคำสั่ง

```
stack new sha1-challenges
```

แล้ว `cd sha1-challenges` เข้าไปใน project

ผมจะใช้ library 3 ตัวคือ

```
    - cryptohash-sha1 เพื่อใช้ sha1 function
    - bytestring เพื่อใช้ type Data.ByteString
    - base16-bytestring เพื่อใช้ encode base16
```

ผมก็แก้ไฟล์ `package.yml` ตรงส่วน dependencies ของ executables แบบนี้


```
executables:
  sha1-challenges-exe:
    main:                Main.hs
    source-dirs:         app
    ghc-options:
    - -threaded
    - -rtsopts
    - -O3
    - -with-rtsopts=-N
    dependencies:
    - sha1-challenges
    - cryptohash-sha1
    - bytestring
    - base16-bytestring
```

ในส่วนของโค้ดที่แก้โจทย์นั้นเขียนในไฟล์ `app/Main.hs` เท่านี้เอง

```haskell
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE NumericUnderscores #-}

module Main where

import Data.ByteString.Base16
import Crypto.Hash.SHA1

loop 5_555_555_555 v = encode $ hash v
loop n v = loop (n+1) (encode $ hash v)

main :: IO ()
main = print $ loop 1 "clubhouse"
```

ผมเปิดใช้ language extension 2 ตัวคือ OverloadedStrings เพื่อให้ใช้ string literal (ตรง double quote `"clubhouse"`) แล้วให้มันแปลงเป็น ByteString ให้เอง และ NumericUnderscores เพื่อให้เขียนตัวเลขโดยใช้ unserscore คั่นได้แบบนี้ 5_555_555_555

จากนั้น `import Data.ByteString.Base16` เพื่อให้ใช้ function `encode` เพื่อแปลงจาก ByteString ที่ได้เป็น base16 format และ `import Crypto.Hash.SHA1` เพื่อใช้ function `hash` ในการ hash แบบ sha1

จากนั้นก็สร้างฟังก์ชัน loop เพื่อ recursive วนซ้ำจาก 1 ถึง 5_555_555_555 โดยแต่ละรอบก็เอาผลลัพธ์ที่ได้ไป hash ต่อๆไป

ใน main ก็แค่เรียก `print $ loop 1 "clubhouse"` เพื่อให้เริ่มที่ 1 ไปเรื่อยๆจนถึง 5_555_555_555 แล้วก็ปริ้นออกมา

วิธี build binary ก็ให้สั่ง

```
stack build --copy-bins --local-bin-path=$PWD
```

แล้วก็รันโดยใช้ time จับเวลาแบบนี้

```
time ./sha1-challenges-exe
```

จากที่ลองวันในเครื่องผมที่เป็น macbook pro รุ่นเก่า 

```
MacBook Pro (Retina, 15-inch, Mid 2014)
Processor: 2.2 GHz Quad-Core Intel Core i7
Memory: 16 GB 1600 MHz DDR3
```

ผลลัพธ์ได้ออกมาแบบนี้

```
time ./sha1-challenges-exe
"87803aa93893f0fc1262b78e879d7a7f7075229a"

real    30m44.986s
user    44m16.479s
sys     5m26.883s
```

ลองเอาโค้ด Go จากใน repository https://github.com/mrchoke/sha1-challages/tree/main/golang/oat มารันดูเวลาที่ใช้ได้เท่านี้

```
time ./sha1
87803aa93893f0fc1262b78e879d7a7f7075229a
Time diff: 19m14.995697452s

real	19m14.953s
user	19m12.227s
sys	0m2.267s
```

ซึ่งถ้าเทียบกันก็ถือว่าไม่แย่ไป ตัว library `cryptohash-sha1` นั้นจริงๆในส่วนของ hash function เขียนด้วย C แล้วใช้วิธี FFI ในการให้โค้ดของ Haskell ไปเรียกใช้งาน C ได้ ส่วนของ Go นั้นเขียนด้วย Go บวกกับ Assembly ที่เจาะจงในแต่ละ CPU architecture ซึ่งช่วยให้ได้ความเร็วที่ดีใช้ได้เลย


## ขอฝาก Buy Me a Coffee
สำหรับท่านใดที่อ่านแล้วชอบโพสต์ต่างๆของผมที่นี่ ต้องการสนับสนุนค่ากาแฟเล็กๆน้อยๆ สามารถสนับสนุนผมได้ผ่านทาง Buy Me a Coffee คลิ๊กที่รูปด้านล่างนี้ได้เลยครับ

<a href="https://www.buymeacoffee.com/weerasak" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" style="height: 60px !important;width: 217px !important;" ></a>

ส่วนท่านใดไม่สะดวกใช้บัตรเครดิต หรือ Paypal สามารถสนับสนุนผมได้ผ่านทาง PromptPay โดยดู QR Code ได้จากโพสต์ที่พินเอาไว้ได้ที่ Page DevDose ครับ https://web.facebook.com/devdoseth

ขอบคุณครับ 🙏