What I think ChatGPT means for developers
Published by Matthew Daly at 11th December 2022 7:20 pm
I'm sure you've heard some of the buzz about ChatGPT. There's been examples of it creating a working Wordpress plugin on demand, recreating the Apple website, acting as a Linux terminal, and writing astonishingly detailed code samples for various purposes on demand.
Naturally I decided to have a tinker with it as I was curious about it, and I've found it very useful for coding. Here are some samples of things I've asked it to do for me.
An example of writing code with ChatGPT
Given the popularity of React.js and my own experience using it, I figured that was something ChatGPT would probably handle OK and that I would be able to evaluate for myself. I therefore asked ChatGPT to create a React-based alert system for me. As I've implemented something similar myself, I was interested to see how well it would do against my own implementation which is in production:
Please create a hook-based toast-style alert system in React.js using hooks
It responded with:
Sure, here is an example of a toast-style alert system in React.js using hooks:
1import React, { useState, useEffect } from 'react';23function useToast() {4 const [isShowing, setIsShowing] = useState(false);5 const [message, setMessage] = useState('');67 function showToast(msg) {8 setMessage(msg);9 setIsShowing(true);10 }1112 function hideToast() {13 setIsShowing(false);14 }1516 useEffect(() => {17 let timeoutId;18 if (isShowing) {19 timeoutId = setTimeout(() => setIsShowing(false), 3000);20 }2122 return () => clearTimeout(timeoutId);23 }, [isShowing]);2425 return {26 isShowing,27 message,28 showToast,29 hideToast30 };31}3233function App() {34 const { isShowing, message, showToast, hideToast } = useToast();3536 return (37 <div>38 {isShowing && (39 <div className="toast">40 <p>{message}</p>41 <button onClick={hideToast}>Close</button>42 </div>43 )}44 <button onClick={() => showToast('This is a toast message!')}>45 Show Toast46 </button>47 </div>48 );49}
In this example, the
useToast
hook manages the state for the toast, including whether it is showing, the message to display, and methods for showing and hiding the toast. TheApp
component uses theuseToast
hook to show and hide the toast when a button is clicked.
The
useToast
hook uses theuseState
hook to manage the state for the toast, and theuseEffect
hook to automatically hide the toast after a certain amount of time. This is just one example of how to implement a toast-style alert system in React.js using hooks, and you can modify it to suit your specific needs.
Looks OK, but it only supports a single message at once, so I followed it up:
Can we amend this to support showing multiple toasts at once?
Yes, it is possible to modify the example to support showing multiple toasts at once. Instead of using a single state value for the message and whether the toast is showing, you can use an array to store multiple toast messages and show them all at once. Here is an example of how you might do this:
1import React, { useState, useEffect } from 'react';23function useToast() {4 const [toasts, setToasts] = useState([]);56 function showToast(msg) {7 setToasts(prevToasts => [...prevToasts, msg]);8 }910 function hideToast(index) {11 setToasts(prevToasts => {12 prevToasts.splice(index, 1);13 return [...prevToasts];14 });15 }1617 useEffect(() => {18 let timeoutIds = [];19 toasts.forEach((_, index) => {20 timeoutIds[index] = setTimeout(() => hideToast(index), 3000);21 });2223 return () => timeoutIds.forEach(timeoutId => clearTimeout(timeoutId));24 }, [toasts]);2526 return {27 toasts,28 showToast,29 hideToast30 };31}3233function App() {34 const { toasts, showToast, hideToast } = useToast();3536 return (37 <div>38 {toasts.map((toast, index) => (39 <div className="toast" key={index}>40 <p>{toast}</p>41 <button onClick={() => hideToast(index)}>Close</button>42 </div>43 ))}44 <button onClick={() => showToast('This is a toast message!')}>45 Show Toast46 </button>47 </div>48 );49}
In this example, the
useToast
hook uses an array to store multiple toast messages, and theApp
component displays all of the toasts in the array. TheuseEffect
hook is used to set a timeout for each toast to automatically hide it after a certain amount of time. You can modify this example further to suit your specific needs, such as setting different timeouts for each toast or adding additional methods for managing the toasts.
Great, although I noticed an unused _
variable in forEach
- not sure if perhaps this is based on something that used Lodash, perhaps.
Next, our toasts need to show different styles based on the type of alert, so I asked this:
Can we amend this to allow the toasts to use different styles based on the type of alert?
Yes, it is possible to modify the example to allow the toasts to use different styles based on the type of alert. You can do this by storing the type of each toast along with the message in the state, and then using this type to determine the style for the toast. Here is an example of how you might do this:
1import React, { useState, useEffect } from 'react';23function useToast() {4 const [toasts, setToasts] = useState([]);56 function showToast(msg, type) {7 setToasts(prevToasts => [...prevToasts, { msg, type }]);8 }910 function hideToast(index) {11 setToasts(prevToasts => {12 prevToasts.splice(index, 1);13 return [...prevToasts];14 });15 }1617 useEffect(() => {18 let timeoutIds = [];19 toasts.forEach((_, index) => {20 timeoutIds[index] = setTimeout(() => hideToast(index), 3000);21 });2223 return () => timeoutIds.forEach(timeoutId => clearTimeout(timeoutId));24 }, [toasts]);2526 return {27 toasts,28 showToast,29 hideToast30 };31}3233function App() {34 const { toasts, showToast, hideToast } = useToast();3536 return (37 <div>38 {toasts.map((toast, index) => (39 <div40 className={`toast toast-${toast.type}`}41 key={index}42 >43 <p>{toast.msg}</p>44 <button onClick={() => hideToast(index)}>Close</button>45 </div>46 ))}47 <button onClick={() => showToast('This is a success toast!', 'success')}>48 Show Success Toast49 </button>50 <button onClick={() => showToast('This is an error toast!', 'error')}>51 Show Error Toast52 </button>53 </div>54 );55}
In this example, the
useToast
hook stores the message and type of each toast in the state, and theApp
component uses the type to determine the class name for the toast. This allows you to define different styles for each type of toast, such as atoast-success
class for success toasts and atoast-error
class for error toasts. You can modify this example further to suit your specific needs, such as defining additional types of toasts or changing the way that the styles are applied.
Great, but a reducer may make more sense than regular state for this use case. Let's ask it to update the code:
Can we amend this code to use a reducer instead of state?
Yes, it is possible to modify the example to use a reducer instead of state. A reducer is a function that takes the current state and an action as arguments, and returns a new state based on the action. This allows you to manage the state for your toast in a more modular and scalable way. Here is an example of how you might do this:
1import React, { useReducer, useEffect } from 'react';23const initialState = {4 toasts: []5};67function reducer(state, action) {8 switch (action.type) {9 case 'SHOW_TOAST':10 return {11 ...state,12 toasts: [...state.toasts, action.payload]13 };14 case 'HIDE_TOAST':15 const newToasts = [...state.toasts];16 newToasts.splice(action.payload, 1);17 return {18 ...state,19 toasts: newToasts20 };21 default:22 return state;23 }24}2526function useToast() {27 const [state, dispatch] = useReducer(reducer, initialState);2829 function showToast(msg, type) {30 dispatch({31 type: 'SHOW_TOAST',32 payload: { msg, type }33 });34 }3536 function hideToast(index) {37 dispatch({38 type: 'HIDE_TOAST',39 payload: index40 });41 }4243 useEffect(() => {44 let timeoutIds = [];45 state.toasts.forEach((_, index) => {46 timeoutIds[index] = setTimeout(() => hideToast(index), 3000);47 });4849 return () => timeoutIds.forEach(timeoutId => clearTimeout(timeoutId));50 }, [state.toasts]);5152 return {53 toasts: state.toasts,54 showToast,55 hideToast56 };57}5859function App() {60 const { toasts, showToast, hideToast } = useToast();6162 return (63 <div>64 {toasts.map((toast, index) => (65 <div66 className={`toast toast-${toast.type}`}67 key={index}68 >69 <p>{toast.msg}</p>70 <button onClick={() => hideToast(index)}>Close</button>71 </div>72 ))}73 <button onClick={() => showToast('This is a success toast!', 'success')}>74 Show Success Toast75 </button>76 <button onClick={() => showToast('This is an error toast!', 'error')}>77 Show Error Toast78 </button>79 </div>80 );81}
In this example, the
useToast
hook uses theuseReducer
hook to manage the state for the toast. The reducer function takes the current state and an action as arguments, and returns a new state based on the action. TheApp
component dispatches actions to show and hide the toast, and theuseEffect
hook is used to set a timeout for each toast to automatically hide it after a certain amount of time
OK, that's done. Now we decide we need this in Typescript:
Please convert this to Typescript
Sure, here is an example of how you might convert the code to TypeScript:
1import React, { useReducer, useEffect } from 'react';23interface ToastState {4 toasts: {5 msg: string;6 type: string;7 }[];8}910const initialState: ToastState = {11 toasts: []12};1314function reducer(state: ToastState, action: { type: string; payload: any }) {15 switch (action.type) {16 case 'SHOW_TOAST':17 return {18 ...state,19 toasts: [...state.toasts, action.payload]20 };21 case 'HIDE_TOAST':22 const newToasts = [...state.toasts];23 newToasts.splice(action.payload, 1);24 return {25 ...state,26 toasts: newToasts27 };28 default:29 return state;30 }31}3233function useToast() {34 const [state, dispatch] = useReducer(reducer, initialState);3536 function showToast(msg: string, type: string) {37 dispatch({38 type: 'SHOW_TOAST',39 payload: { msg, type }40 });41 }4243 function hideToast(index: number) {44 dispatch({45 type: 'HIDE_TOAST',46 payload: index47 });48 }4950 useEffect(() => {51 let timeoutIds = [];52 state.toasts.forEach((_, index) => {53 timeoutIds[index] = setTimeout(() => hideToast(index), 3000);54 });5556 return () => timeoutIds.forEach(timeoutId => clearTimeout(timeoutId));57 }, [state.toasts]);5859 return {60 toasts: state.toasts,61 showToast,62 hideToast63 };64}6566function App() {67 const { toasts, showToast, hideToast } = useToast();6869 return (70 <div>71 {toasts.map((toast, index) => (72 <div73 className={`toast toast-${toast.type}`}74 key={index}75 >76 <p>{toast.msg}</p>77 <button onClick={() => hideToast(index)}>Close</button>78 </div>79 ))}80 <button onClick={() => showToast('This is a success toast!', 'success')}>81 Show Success Toast82 </button>83 <button onClick={() => showToast('This is an error toast!', 'error')}>84 Show Error Toast85 </button>86 </div>87 );88}
In this example, the code is written in TypeScript, and type annotations have been added to the functions and variables to specify the types of their arguments and return values. This allows the TypeScript compiler to check the code for type errors, and provides additional type information to other tools and libraries that can make use of it. You can modify this example further to suit your specific needs, such as adding additional types or using more advanced type features.
Next, can it refactor this?
Could we refactor this?
Yes, it is possible to refactor the code to make it more concise and modular. Here is an example of how you might do this:
1import React, { useReducer, useEffect } from 'react';23interface Toast {4 msg: string;5 type: string;6}78type State = Toast[];910type Action =11 | { type: 'SHOW_TOAST'; payload: Toast }12 | { type: 'HIDE_TOAST'; payload: number };1314const initialState: State = [];1516function reducer(state: State, action: Action): State {17 switch (action.type) {18 case 'SHOW_TOAST':19 return [...state, action.payload];20 case 'HIDE_TOAST':21 const newToasts = [...state];22 newToasts.splice(action.payload, 1);23 return newToasts;24 default:25 return state;26 }27}2829function useToast() {30 const [toasts, dispatch] = useReducer(reducer, initialState);3132 useEffect(() => {33 const timeoutIds = toasts.map((_, index) =>34 setTimeout(() => dispatch({ type: 'HIDE_TOAST', payload: index }), 3000)35 );36 return () => timeoutIds.forEach(clearTimeout);37 }, [toasts]);3839 return {40 toasts,41 showToast: (msg: string, type: string) =>42 dispatch({ type: 'SHOW_TOAST', payload: { msg, type } }),43 hideToast: (index: number) =>44 dispatch({ type: 'HIDE_TOAST', payload: index })45 };46}4748function App() {49 const { toasts, showToast, hideToast } = useToast();5051 return (52 <div>53 {toasts.map((toast, index) => (54 <div55 className={`toast toast-${toast.type}`}56 key={index}57 >58 <p>{toast.msg}</p>59 <button onClick={() => hideToast(index)}>Close</button>60 </div>61 ))}62 <button onClick={() => showToast('This is a success toast!', 'success')}>63 Show Success Toast64 </button>65 <button onClick={() => showToast('This is an error toast!', 'error')}>66 Show Error Toast67 </button>68 </div>69 );70}
In this example, the code has been refactored to use more concise and modular functions and types. The reducer function has been simplified and made more type-safe by using a union type for the
Action
type, and theuseToast
hook has been refactored to use the useReducer hook more effectively and efficiently. Additionally, the code has been cleaned up and made more readable by using modern JavaScript features and conventions. You can modify this example further to suit your specific
Great. Now, we should really write a test for this. Let's see if ChatGPT can do that for us:
How might we test this hook with Jest?
To test the
useToast
hook with Jest, you can write a test that uses the act function from the@testing-library/react
library to simulate user interactions with the component and verify that the hook behaves as expected. Here is an example of how you might write a test for theuseToast
hook:
1import React from 'react';2import { act, renderHook } from '@testing-library/react-hooks';3import { useToast } from './useToast';45describe('useToast', () => {6 it('should show and hide toasts', () => {7 const { result } = renderHook(() => useToast());8 const { toasts, showToast, hideToast } = result.current;910 // Initially, there should be no toasts11 expect(toasts).toEqual([]);1213 // Show a toast and verify that it appears in the state14 act(() => showToast('This is a test toast', 'success'));15 expect(toasts).toEqual([{ msg: 'This is a test toast', type: 'success' }]);1617 // Hide the toast and verify that it disappears from the state18 act(() => hideToast(0));19 expect(toasts).toEqual([]);20 });21});
In this test, the
renderHook
function from the@testing-library/react-hooks
library is used to render theuseToast
hook in a test environment. The test then simulates user interactions with the hook by calling theshowToast
andhideToast
functions, and verifies that the toasts are shown and hidden as expected. This test can be further extended to cover additional scenarios and edge cases, such as showing multiple toasts at once or verifying the behavior of the hook when an error occurs.
Overall, this looks pretty good! I'd need to spend a lot more time than I can spare now to try to find if there are any issues with it, and it wouldn't surprise me if there were a few bugs, but it looks fairly sounds as an example.
Getting examples of the use of a technique
Another thing that ChatGPT seems useful for is creating examples. For instance, I've never had the occasion to use render props in React, so I asked ChatGPT the following question:
Please demonstrate the use of render props with functional React components
Sure! In React, a "render prop" is a technique for sharing code between React components using a prop whose value is a function. This function is used to render the content of the component. Using a render prop with a functional component is no different than using it with a class-based component. Here's an example:
1function MyComponent(props) {2 return (3 <div>4 {props.render(props.value)}5 </div>6 );7}
In the above example, MyComponent is a functional component that has a render prop, which is a function that is used to render the content of the component. The render prop takes a value prop as an argument, which can be used in the rendering of the content.
Here's an example of how you might use this component:
1<MyComponent2 value={5}3 render={value => (4 <div>5 The value is {value}.6 </div>7 )}8/>
In this example, the value passed to the value prop is 5, and the render prop is a function that returns a div element with the text "The value is 5".
In summary, using a render prop with a functional component is just a matter of using a prop whose value is a function, and using that function to render the content of the component.
I personally find this is a better, more concise example than the one in the React documentation, so I'm quite pleased with that as an example. Now let's ask for them in Typescript:
Please convert these examples to Typescript
Sure, here are the examples written in TypeScript:
1function MyComponent(props: { value: any; render: (value: any) => React.ReactNode }) {2 return (3 <div>4 {props.render(props.value)}5 </div>6 );7}
1<MyComponent2 value={5}3 render={(value: any) => (4 <div>5 The value is {value}.6 </div>7 )}8/>
In the first example, the MyComponent function is defined to take an object with two properties: value and render. The value property is of type any, which means it can be any value. The render property is a function that takes a value of type any and returns a React.ReactNode, which is the type of value that can be rendered by a React component.
In the second example, the value prop is passed the value 5, which is a number. The render prop is passed a function that takes a value of type any and returns a div element.
I hope this helps! Let me know if you have any other questions.
Nice one!
Refactoring from a legacy framework
I thought I'd try another example that was more immediately useful to me. I work on a large legacy Zend 1 framework which I'm slowly trying to modernize, and a big chunk of the models have been migrated to Eloquent, but the controllers are the biggest blocker by far to migrating to a newer framework, and as long as they're in use we can't migrate easily migrate some other parts, such as the forms. I wrote an example Zend 1 controller and asked ChatGPT the following question:
Given the following Zend 1 controller:
1<?php23use App\Forms\CategoryForm;4use App\Models\Category;56final class CategoriesController extends Zend_Controller_Action7{8 public function indexAction(): void9 {10 $this->view->categories = Category::all();11 }1213 public function addAction(): void14 {15 $request = $this->getRequest();16 assert($request instanceof Zend_Http_Request_Http);17 if (!$request->isPost()) {18 throw new Zend_Controller_Action_Exception("Unsupported method", 405);19 }20 $form = new CategoryForm();21 if (!$form->isValid($request->getPost())) {22 throw new Zend_Controller_Action_Exception("Invalid data", 422);23 }24 $category = Category::create([25 'name' => $form->name->getValue(),26 ]);27 $this->view->category = $category;28 }29}
Please convert it to a Laravel controller
It came back with:
Here is a possible conversion of the given Zend 1 controller to a Laravel controller:
1<?php23use App\Http\Controllers\Controller;4use App\Forms\CategoryForm;5use App\Models\Category;6use Illuminate\Http\Request;78class CategoriesController extends Controller9{10 public function index(): void11 {12 $categories = Category::all();13 return view('categories.index', compact('categories'));14 }1516 public function add(Request $request): void17 {18 if (!$request->isMethod('post')) {19 abort(405, 'Unsupported method');20 }21 $form = new CategoryForm();22 if (!$form->isValid($request->all())) {23 abort(422, 'Invalid data');24 }25 $category = Category::create([26 'name' => $form->name->getValue(),27 ]);28 return view('categories.add', compact('category'));29 }30}
Note that this is just one possible way to convert the given code, and there may be other ways to accomplish the same thing using Laravel's features and conventions.
This looks pretty good to me. It does still depend on the Zend form classes, but once the controller has been rewritten for Laravel, it would be easy to replace them with form request classes. While this is a very minimal example, it shows the potential power of ChatGPT in terms of migrating legacy code to more modern frameworks. I'm sure it's not always going to be 100% accurate, but other tools like Psalm, which I use anyway, will help find potential problems with method names or arguments being wrong, as will peer reviews of the code, and it'd certainly be less laborious than doing it all manually.
Writing website copy
I asked it the following question:
Write the homepage copy for a website proving tips and tricks with performance improvements for web applications built with Laravel
And it came back with:
Welcome to Tips and Tricks for Laravel Performance!
Are you looking to improve the performance of your web application built with Laravel? You've come to the right place!
Our website provides a wealth of information and tips on how to optimize your Laravel application for maximum performance. From caching and database optimization, to code refactoring and route optimization, we've got you covered.
We also offer in-depth tutorials and guides on how to implement these performance improvements in your own Laravel application.
Don't let slow performance hold your web application back. Take control and improve your Laravel application's performance today with the help of Tips and Tricks for Laravel Performance.
Not bad, I've certainly seen a lot worse than that from human writers. Next, I asked it to write a blog post for the site:
Now write a blog post for this site on how to improve the performance of the site by setting appropriate HTTP headers, with code samples
Improving the performance of your Laravel application starts with understanding the role of HTTP headers. HTTP headers are key-value pairs that are sent in the header of an HTTP request or response, and provide important information about the request or response.
One of the ways to improve the performance of your Laravel application is to set appropriate HTTP headers. This allows the client, such as a web browser, to make more informed decisions about how to handle the request or response.
For example, setting the Cache-Control header can improve performance by allowing the client to cache the response, so that it doesn't have to be fetched again on subsequent requests. This can be done in Laravel using the Response facade:
1use Illuminate\Support\Facades\Response;23// Set the Cache-Control header to cache the response for 1 hour4Response::header('Cache-Control', 'max-age=3600');
Another important header for improving performance is the Expires header. This header specifies a date and time at which the response will no longer be considered fresh. This allows the client to make an informed decision about whether to use a cached version of the response or fetch a new one.
In Laravel, the Expires header can be set using the Response facade:
1use Carbon\Carbon;2use Illuminate\Support\Facades\Response;34// Set the Expires header to one hour from now5$expires = Carbon::now()->addHour();6Response::header('Expires', $expires->toRfc2822String());
By setting these headers, you can improve the performance of your Laravel application by allowing the client to make more efficient use of caching. This can reduce the number of requests that need to be made to the server, resulting in faster load times and a better user experience.
Of course, there are many other HTTP headers that can be used to improve the performance of your Laravel application. For more information, check out the official Laravel documentation on HTTP headers.
Not hugely comprehensive, but a reasonably good starting point. Using this to get you started seems like a good idea, and there's nothing more intimidating than a blank page when you want to write a blog post.
Overall impressions
Even in its current state as a tech preview, ChatGPT is hugely impressive. It's extraordinary how good it is at generating basic code samples for almost any use case I can think of. However, the distinct impression I get is that it's not really capable of original thought. Most of the above is basically just taking stuff that's already out there and essentially remixing it. There's plenty of tutorials on writing all of the above and it looks to me like it's just taking those and converting those according to the provided specifications.
However, don't get the impression that doesn't make it useful. I've used Github Copilot for a while and that offers dramatic improvements in productivity, and this has the potential to be much more significant. As shown above, it can migrate a simple controller from Zend 1 to Laravel, with the result looking perfectly acceptable, and the value of that should be clear to anyone who has to work on a large legacy project. Similarly you can ask it to rewrite something to use a specific design pattern and the end result is perfectly acceptable. It's a fantastic learning tool too since you can ask it to provide an example specific to your use case rather than trawling through blog posts, where most of them might have different requirements to you. As such it's going to hugely improve developer productivity.
To a large extent, I think something like this is going change the developer's role by shifting the emphasis of it somewhat from the productive to the executive, with our job being more akin to editors than authors. Coding will become less about solving the problem and more about defining the problem in the first place and letting the system solve it. However, I don't think it will automate our jobs away entirely - the quality of the code it produces is variable, to say the least, and there will definitely be a need to be able to debug it, and solve more unusual problems.
However, if my job involved writing spammy blog posts for ad money, I'd be very worried about my future prospects. I think tools like this will probably be writing most of the spammier sort of blog posts that just exist to get clicks on the web in a year at most. I've actually just asked it to write a blog post I've wanted to write for a while now for me, and the end result looks pretty solid right now - I'd anticipate it saved me about 90% of the work, with the remaining 10% just a case of cleaning it up.