forked from miciek/grokkingfp-examples
-
Notifications
You must be signed in to change notification settings - Fork 0
/
ch08_SchedulingMeetingsImpure.java
199 lines (178 loc) · 8.63 KB
/
ch08_SchedulingMeetingsImpure.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
import java.util.*;
public class ch08_SchedulingMeetingsImpure {
/**
* PREREQUISITE 1: MeetingTime model
* <p>
* We use MeetingTime defined here in both Java and Scala versions.
* Note this is written in Java, but in the spirit of immutability.
* <p>
* In Scala, this looks like:
* case class MeetingTime(startHour: Int, endHour: Int)
*/
static class MeetingTime { // or: record MeetingTime(int startHour, int endHour) {};
public final int startHour;
public final int endHour;
MeetingTime(int startHour, int endHour) {
this.startHour = startHour;
this.endHour = endHour;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
MeetingTime that = (MeetingTime) o;
return startHour == that.startHour &&
endHour == that.endHour;
}
@Override
public int hashCode() {
return Objects.hash(startHour, endHour);
}
@Override
public String toString() {
return "MeetingTime[" +
"startHour=" + startHour +
", endHour=" + endHour +
']';
}
}
// In modern Java you can also replace this class with the record type
// record MeetingTime(int startHour, int endHour) {};
/*
* PREREQUISITE 2: unsafe functions that we'll use to simulate external API calls
*
* They have *ApiCall suffix:
* - call an external service to all current calendar entries for a given name.
* - call an external service to create a new meeting in $name's calendar.
*
* These are not an example of FP code, but a simplistic simulation of how some an external API may behave.
* Note that we can't change an external API and need to work with how it works (so no change in these functions!).
* Note that we don't consider any security pitfalls her for the sake of a cleaner presentation.
* Note that most likely this should return a raw JSON, not a List[Meeting], but it doesn't matter here, plus
* we already know how to deal with parsing.
*/
static List<MeetingTime> calendarEntriesApiCall(String name) {
Random rand = new Random();
if (rand.nextFloat() < 0.25) throw new RuntimeException("Connection error");
if (name.equals("Alice")) return List.of(new MeetingTime(8, 10), new MeetingTime(11, 12));
else if (name.equals("Bob")) return List.of(new MeetingTime(9, 10));
else { // random meeting starting between 8 and 12, and ending between 13 and 16
return List.of(new MeetingTime(rand.nextInt(5) + 8, rand.nextInt(4) + 13));
}
}
static void createMeetingApiCall(List<String> names, MeetingTime meetingTime) {
// Note that it also may fail fail, similarly to calendarEntriesApiCall, but we don't show it in the book:
// Random rand = new Random();
// if(rand.nextFloat() < 0.25) throw new RuntimeException("💣");
System.out.printf("SIDE-EFFECT: Created meeting %s for %s\n", meetingTime, Arrays.toString(names.toArray()));
}
/**
* Less evil version of calendarEntriesApiCall used to show the business logic of an imperatively
* written schedule function.
*/
private static List<MeetingTime> calendarEntriesApiCallNoFailures(String name) {
if (name.equals("Alice")) return List.of(new MeetingTime(8, 10), new MeetingTime(11, 12));
else if (name.equals("Bob")) return List.of(new MeetingTime(9, 10));
else { // random meeting starting between 8 and 12, and ending between 13 and 16
Random rand = new Random();
return List.of(new MeetingTime(rand.nextInt(5) + 8, rand.nextInt(4) + 13));
}
}
/**
* STEP 0: imperative implementation of the happy path (assuming no failures)
*
* For demonstration purposes we are showing a slightly different version than in the book. It's a
* less evil version of the proper imperative schedule function (below) used to show happy path of the business logic.
* API calls don't throw any errors, but they still may return different results for the same parameters (randomly).
*/
static MeetingTime scheduleNoFailures(String person1, String person2, int lengthHours) {
List<MeetingTime> person1Entries = calendarEntriesApiCallNoFailures(person1); // this is different than in the book
List<MeetingTime> person2Entries = calendarEntriesApiCallNoFailures(person2); // the book version fails a lot!
List<MeetingTime> scheduledMeetings = new ArrayList<>();
scheduledMeetings.addAll(person1Entries);
scheduledMeetings.addAll(person2Entries);
List<MeetingTime> slots = new ArrayList<>();
for (int startHour = 8; startHour < 16 - lengthHours + 1; startHour++) {
slots.add(new MeetingTime(startHour, startHour + lengthHours));
}
List<MeetingTime> possibleMeetings = new ArrayList<>();
for (var slot : slots) {
var meetingPossible = true;
for (var meeting : scheduledMeetings) {
if (slot.endHour > meeting.startHour && meeting.endHour > slot.startHour) {
meetingPossible = false;
break;
}
}
if (meetingPossible) {
possibleMeetings.add(slot);
}
}
if (!possibleMeetings.isEmpty()) {
createMeetingApiCall(List.of(person1, person2), possibleMeetings.get(0));
return possibleMeetings.get(0);
} else return null;
}
/**
* Proper version of imperatively written schedule function with one-retry recovery strategy.
* Uses calendarEntriesApiCall that may fail.
*/
static MeetingTime schedule(String person1, String person2, int lengthHours) {
List<MeetingTime> person1Entries = null;
try {
person1Entries = calendarEntriesApiCall(person1);
} catch (Exception e) {
// retry:
person1Entries = calendarEntriesApiCall(person1);
}
List<MeetingTime> person2Entries = null;
try {
person2Entries = calendarEntriesApiCall(person2);
} catch (Exception e) {
// retry:
person2Entries = calendarEntriesApiCall(person2);
}
List<MeetingTime> scheduledMeetings = new ArrayList<>();
scheduledMeetings.addAll(person1Entries);
scheduledMeetings.addAll(person2Entries);
List<MeetingTime> slots = new ArrayList<>();
for (int startHour = 8; startHour < 16 - lengthHours + 1; startHour++) {
slots.add(new MeetingTime(startHour, startHour + lengthHours));
}
List<MeetingTime> possibleMeetings = new ArrayList<>();
for (var slot : slots) {
var meetingPossible = true;
for (var meeting : scheduledMeetings) {
if (slot.endHour > meeting.startHour && meeting.endHour > slot.startHour) {
meetingPossible = false;
break;
}
}
if (meetingPossible) {
possibleMeetings.add(slot);
}
}
if (!possibleMeetings.isEmpty()) {
createMeetingApiCall(List.of(person1, person2), possibleMeetings.get(0));
return possibleMeetings.get(0);
} else return null;
}
public static void main(String[] args) {
assert(scheduleNoFailures("Alice", "Bob", 1).equals(new MeetingTime(10, 11)));
assert(scheduleNoFailures("Alice", "Bob", 2).equals(new MeetingTime(12, 14)));
assert(scheduleNoFailures("Alice", "Bob", 3).equals(new MeetingTime(12, 15)));
assert(scheduleNoFailures("Alice", "Bob", 4).equals(new MeetingTime(12, 16)));
assert(scheduleNoFailures("Alice", "Bob", 5) == null);
scheduleNoFailures("Alice", "Charlie", 2); // we don't know Charlie's schedule, so can't assert
try {
assert(schedule("Alice", "Bob", 1).equals(new MeetingTime(10, 11)));
assert(schedule("Alice", "Bob", 2).equals(new MeetingTime(12, 14)));
assert(schedule("Alice", "Bob", 3).equals(new MeetingTime(12, 15)));
assert(schedule("Alice", "Bob", 4).equals(new MeetingTime(12, 16)));
assert(schedule("Alice", "Bob", 5) == null);
schedule("Alice", "Charlie", 2); // we don't know Charlie's schedule, so can't assert
} catch (Throwable t) {
System.out.println("Caught an exception in the impure version: " + t.getMessage());
}
}
}