monads

Monads, For Golang, Using Generics
git clone git://git.lair.cx/monads
Log | Files | Refs | README | LICENSE

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:
A.gitignore | 217+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Afutures/future.go | 132+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Afutures/future_test.go | 125+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ago.mod | 3+++
Aoptions/option.go | 39+++++++++++++++++++++++++++++++++++++++
Aoptions/option_test.go | 78++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aslices/filter.go | 12++++++++++++
Aslices/map.go | 17+++++++++++++++++
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 +}