commit 5e265f180e96ceb4317ff2e4a8ef7a4eff80b998
Author: Yongbin Kim <iam@yongbin.kim>
Date: Mon, 12 Sep 2022 14:36:45 +0900
migrated
Signed-off-by: Yongbin Kim <iam@yongbin.kim>
Diffstat:
8 files changed, 623 insertions(+), 0 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -0,0 +1,216 @@
+# Created by https://www.toptal.com/developers/gitignore/api/linux,macos,windows,goland,visualstudiocode
+# Edit at https://www.toptal.com/developers/gitignore?templates=linux,macos,windows,goland,visualstudiocode
+
+### GoLand ###
+# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
+# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
+
+# User-specific stuff
+.idea/**/workspace.xml
+.idea/**/tasks.xml
+.idea/**/usage.statistics.xml
+.idea/**/dictionaries
+.idea/**/shelf
+
+# AWS User-specific
+.idea/**/aws.xml
+
+# Generated files
+.idea/**/contentModel.xml
+
+# Sensitive or high-churn files
+.idea/**/dataSources/
+.idea/**/dataSources.ids
+.idea/**/dataSources.local.xml
+.idea/**/sqlDataSources.xml
+.idea/**/dynamic.xml
+.idea/**/uiDesigner.xml
+.idea/**/dbnavigator.xml
+
+# Gradle
+.idea/**/gradle.xml
+.idea/**/libraries
+
+# Gradle and Maven with auto-import
+# When using Gradle or Maven with auto-import, you should exclude module files,
+# since they will be recreated, and may cause churn. Uncomment if using
+# auto-import.
+# .idea/artifacts
+# .idea/compiler.xml
+# .idea/jarRepositories.xml
+# .idea/modules.xml
+# .idea/*.iml
+# .idea/modules
+# *.iml
+# *.ipr
+
+# CMake
+cmake-build-*/
+
+# Mongo Explorer plugin
+.idea/**/mongoSettings.xml
+
+# File-based project format
+*.iws
+
+# IntelliJ
+out/
+
+# mpeltonen/sbt-idea plugin
+.idea_modules/
+
+# JIRA plugin
+atlassian-ide-plugin.xml
+
+# Cursive Clojure plugin
+.idea/replstate.xml
+
+# SonarLint plugin
+.idea/sonarlint/
+
+# Crashlytics plugin (for Android Studio and IntelliJ)
+com_crashlytics_export_strings.xml
+crashlytics.properties
+crashlytics-build.properties
+fabric.properties
+
+# Editor-based Rest Client
+.idea/httpRequests
+
+# Android studio 3.1+ serialized cache file
+.idea/caches/build_file_checksums.ser
+
+### GoLand Patch ###
+# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
+
+# *.iml
+# modules.xml
+# .idea/misc.xml
+# *.ipr
+
+# Sonarlint plugin
+# https://plugins.jetbrains.com/plugin/7973-sonarlint
+.idea/**/sonarlint/
+
+# SonarQube Plugin
+# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin
+.idea/**/sonarIssues.xml
+
+# Markdown Navigator plugin
+# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced
+.idea/**/markdown-navigator.xml
+.idea/**/markdown-navigator-enh.xml
+.idea/**/markdown-navigator/
+
+# Cache file creation bug
+# See https://youtrack.jetbrains.com/issue/JBR-2257
+.idea/$CACHE_FILE$
+
+# CodeStream plugin
+# https://plugins.jetbrains.com/plugin/12206-codestream
+.idea/codestream.xml
+
+# Azure Toolkit for IntelliJ plugin
+# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij
+.idea/**/azureSettings.xml
+
+### Linux ###
+*~
+
+# temporary files which can be created if a process still has a handle open of a deleted file
+.fuse_hidden*
+
+# KDE directory preferences
+.directory
+
+# Linux trash folder which might appear on any partition or disk
+.Trash-*
+
+# .nfs files are created when an open file is removed but is still being accessed
+.nfs*
+
+### macOS ###
+# General
+.DS_Store
+.AppleDouble
+.LSOverride
+
+# Icon must end with two \r
+Icon
+
+# Thumbnails
+._*
+
+# Files that might appear in the root of a volume
+.DocumentRevisions-V100
+.fseventsd
+.Spotlight-V100
+.TemporaryItems
+.Trashes
+.VolumeIcon.icns
+.com.apple.timemachine.donotpresent
+
+# Directories potentially created on remote AFP share
+.AppleDB
+.AppleDesktop
+Network Trash Folder
+Temporary Items
+.apdisk
+
+### macOS Patch ###
+# iCloud generated files
+*.icloud
+
+### VisualStudioCode ###
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+!.vscode/*.code-snippets
+
+# Local History for Visual Studio Code
+.history/
+
+# Built Visual Studio Code Extensions
+*.vsix
+
+### VisualStudioCode Patch ###
+# Ignore all local history of files
+.history
+.ionide
+
+# Support for Project snippet scope
+.vscode/*.code-snippets
+
+# Ignore code-workspaces
+*.code-workspace
+
+### Windows ###
+# Windows thumbnail cache files
+Thumbs.db
+Thumbs.db:encryptable
+ehthumbs.db
+ehthumbs_vista.db
+
+# Dump file
+*.stackdump
+
+# Folder config file
+[Dd]esktop.ini
+
+# Recycle Bin used on file shares
+$RECYCLE.BIN/
+
+# Windows Installer files
+*.cab
+*.msi
+*.msix
+*.msm
+*.msp
+
+# Windows shortcuts
+*.lnk
+
+# End of https://www.toptal.com/developers/gitignore/api/linux,macos,windows,goland,visualstudiocode
+n
+\ No newline at end of file
diff --git a/futures/future.go b/futures/future.go
@@ -0,0 +1,132 @@
+package futures
+
+import (
+ "errors"
+ "sync"
+ "sync/atomic"
+)
+
+var (
+ ErrNotDone = errors.New("not done yet")
+
+ closedChan = make(chan struct{})
+)
+
+func init() {
+ close(closedChan)
+}
+
+type Future[T any] struct {
+ mu sync.Mutex
+ done atomic.Value
+
+ isDone bool
+ value T
+ error error
+}
+
+func Run[T any](handler func() (T, error)) *Future[T] {
+ f := &Future[T]{}
+
+ go func() {
+ value, err := handler()
+
+ f.mu.Lock()
+ if ch, ok := f.done.Load().(chan struct{}); ok {
+ close(ch)
+ }
+ f.isDone = true
+ if err != nil {
+ f.error = err
+ } else {
+ f.value = value
+ }
+ f.mu.Unlock()
+ }()
+
+ return f
+}
+
+func Resolve[T any](value T) *Future[T] {
+ return &Future[T]{
+ value: value,
+ isDone: true,
+ }
+}
+
+func Reject[T any](err error) *Future[T] {
+ return &Future[T]{
+ error: err,
+ isDone: true,
+ }
+}
+
+func (f *Future[T]) Done() <-chan struct{} {
+ f.mu.Lock()
+ defer f.mu.Unlock()
+
+ return f.unsafeDone()
+}
+
+func Done[T any](f *Future[T]) <-chan struct{} {
+ return f.Done()
+}
+
+func (f *Future[T]) Await() (value T, err error) {
+ f.mu.Lock()
+ done := f.unsafeDone()
+ f.mu.Unlock()
+
+ // Wait until the task is done
+ _, _ = <-done
+
+ return f.value, f.error
+}
+
+func Await[T any](f *Future[T]) (T, error) {
+ return f.Await()
+}
+
+func (f *Future[T]) Unwrap() (value T, err error) {
+ if !f.isDone {
+ err = ErrNotDone
+ return
+ }
+
+ return f.value, f.error
+}
+
+func Unwrap[T any](f *Future[T]) (T, error) {
+ return f.Unwrap()
+}
+
+func Map[T, R any](f *Future[T], action func(T) (R, error)) *Future[R] {
+ return Run[R](func() (result R, err error) {
+ var value T
+ value, err = f.Await()
+ if err != nil {
+ return
+ }
+ return action(value)
+ })
+}
+
+func FlatMap[T, R any](f *Future[T], action func(T) *Future[R]) *Future[R] {
+ return Map[T, R](f, func(value T) (R, error) {
+ return action(value).Await()
+ })
+}
+
+func (f *Future[T]) unsafeDone() <-chan struct{} {
+ if f.isDone {
+ return closedChan
+ }
+
+ d := f.done.Load()
+ if d == nil {
+ d = make(chan struct{})
+ f.done.Store(d)
+ }
+
+ return d.(chan struct{})
+}
diff --git a/futures/future_test.go b/futures/future_test.go
@@ -0,0 +1,125 @@
+package futures
+
+import (
+ "errors"
+ "testing"
+ "time"
+)
+
+var (
+ rejectedErr = errors.New("rejected")
+)
+
+func action(v bool) (bool, error) {
+ return !v, nil
+}
+
+func heavyAction(v bool) *Future[bool] {
+ time.Sleep(100 * time.Millisecond)
+ return Resolve(!v)
+}
+
+func TestFuture(t *testing.T) {
+ tests := []struct {
+ name string
+ handler func() (int, error)
+ reject bool
+ }{
+ {
+ "should be resolved",
+ func() (int, error) {
+ time.Sleep(100 * time.Millisecond)
+ return 42, nil
+ },
+ false,
+ },
+ {
+ "should be rejected",
+ func() (int, error) {
+ time.Sleep(100 * time.Millisecond)
+ return 0, errors.New("rejected")
+ },
+ true,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ gotValue, err := Await(Run(tt.handler))
+ if tt.reject {
+ if err == nil {
+ t.Errorf("should be rejected")
+ }
+ return
+ }
+ if gotValue != 42 {
+ t.Errorf("gotValue = %v, want 42", gotValue)
+ }
+ })
+ }
+}
+
+func TestMap(t *testing.T) {
+ tests := []struct {
+ name string
+ f *Future[bool]
+ want *Future[bool]
+ }{
+ {
+ "resolved then",
+ Resolve(false),
+ Resolve(true),
+ },
+ {
+ "rejected then",
+ Reject[bool](rejectedErr),
+ Reject[bool](rejectedErr),
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ f := Map(tt.f, action)
+ got, err := Await(f)
+ want, wantErr := Unwrap(tt.want)
+ if err != wantErr {
+ t.Errorf("err = %v, want %v", err, wantErr)
+ return
+ }
+ if got != want {
+ t.Errorf("got = %v, want %v", got, want)
+ }
+ })
+ }
+}
+
+func TestFlatMap(t *testing.T) {
+ tests := []struct {
+ name string
+ f *Future[bool]
+ want *Future[bool]
+ }{
+ {
+ "resolved then",
+ Resolve(false),
+ Resolve(true),
+ },
+ {
+ "rejected then",
+ Reject[bool](rejectedErr),
+ Reject[bool](rejectedErr),
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ f := FlatMap(tt.f, heavyAction)
+ got, err := Await(f)
+ want, wantErr := Unwrap(tt.want)
+ if err != wantErr {
+ t.Errorf("err = %v, want %v", err, wantErr)
+ return
+ }
+ if got != want {
+ t.Errorf("got = %v, want %v", got, want)
+ }
+ })
+ }
+}
diff --git a/go.mod b/go.mod
@@ -0,0 +1,3 @@
+module go.lair.cx/monads
+
+go 1.18
diff --git a/options/option.go b/options/option.go
@@ -0,0 +1,39 @@
+package options
+
+type Option[T any] struct {
+ value T
+ isValid bool
+}
+
+func Empty[T any]() Option[T] {
+ return Option[T]{
+ isValid: false,
+ }
+}
+
+func Wrap[T any](value T) Option[T] {
+ return Option[T]{
+ value: value,
+ isValid: true,
+ }
+}
+
+func (v Option[T]) Unwrap() T {
+ return v.value
+}
+
+func Map[T, R any](value Option[T], action func(T) R) Option[R] {
+ if !value.isValid {
+ return Empty[R]()
+ }
+
+ return Wrap[R](action(value.value))
+}
+
+func FlatMap[T, R any](value Option[T], action func(T) Option[R]) Option[R] {
+ if !value.isValid {
+ return Empty[R]()
+ }
+
+ return action(value.value)
+}
diff --git a/options/option_test.go b/options/option_test.go
@@ -0,0 +1,78 @@
+package options
+
+import (
+ "reflect"
+ "strconv"
+ "testing"
+)
+
+func TestMap(t *testing.T) {
+ action := func(s string) int {
+ n, err := strconv.ParseInt(s, 10, 64)
+ if err != nil {
+ panic(err)
+ }
+ return int(n)
+ }
+ tests := []struct {
+ name string
+ value Option[string]
+ want Option[int]
+ }{
+ {
+ "with non-empty value",
+ Wrap("1"),
+ Wrap(1),
+ },
+ {
+ "with empty value",
+ Empty[string](),
+ Empty[int](),
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if got := Map(tt.value, action); !reflect.DeepEqual(got, tt.want) {
+ t.Errorf("Map() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
+
+func TestFlatMap(t *testing.T) {
+ action := func(s string) Option[int] {
+ n, err := strconv.ParseInt(s, 10, 64)
+ if err != nil {
+ return Empty[int]()
+ }
+ return Wrap(int(n))
+ }
+ tests := []struct {
+ name string
+ value Option[string]
+ want Option[int]
+ }{
+ {
+ "with non-empty value",
+ Wrap("1"),
+ Wrap(1),
+ },
+ {
+ "with empty value",
+ Empty[string](),
+ Empty[int](),
+ },
+ {
+ "with invalid value",
+ Wrap("??"),
+ Empty[int](),
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if got := FlatMap(tt.value, action); !reflect.DeepEqual(got, tt.want) {
+ t.Errorf("Map() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
diff --git a/slices/filter.go b/slices/filter.go
@@ -0,0 +1,12 @@
+package slices
+
+func Filter[T any](items []T, action func(T) bool) (result []T) {
+ result = make([]T, 0, len(items))
+ for _, item := range items {
+ if !action(item) {
+ continue
+ }
+ result = append(result, item)
+ }
+ return
+}
diff --git a/slices/map.go b/slices/map.go
@@ -0,0 +1,17 @@
+package slices
+
+func Map[T any, R any](m []T, action func(T) R) (result []R) {
+ result = make([]R, len(m))
+ for i, item := range m {
+ result[i] = action(item)
+ }
+ return
+}
+
+func FlatMap[T any, R any](m []T, action func(T) []R) (result []R) {
+ result = make([]R, 0, len(m))
+ for _, item := range m {
+ result = append(result, action(item)...)
+ }
+ return
+}